From 8302aa847e518ee00b5d70e9368381e229c52728 Mon Sep 17 00:00:00 2001 From: Bhavesh Patel Date: Wed, 9 Sep 2026 16:24:25 +0530 Subject: [PATCH 1/8] Persist top-level agent description --- .changeset/agent-description.md | 5 +++ packages/trueforge/src/apis/agents.ts | 3 ++ packages/trueforge/src/db/agentStore.ts | 9 +++-- .../agent-store/PostgresAgentStore.ts | 7 +++- .../20260909_000001_agent_description.ts | 16 ++++++++ packages/trueforge/src/db/postgres/types.ts | 1 + .../db/sqlite/agent-store/SqliteAgentStore.ts | 8 +++- .../20260909_000001_agent_description.ts | 15 +++++++ packages/trueforge/src/db/sqlite/types.ts | 1 + packages/trueforge/src/routes/agentRoutes.ts | 2 +- packages/trueforge/src/schemas/agent.ts | 14 ++++++- .../src/truefoundry/TrueFoundryAgentStore.ts | 40 ++++++++++++++----- .../tests/db/agentStoreContractSuite.ts | 23 +++++++++++ .../tests/db/scheduleDispatchContractSuite.ts | 1 + .../tests/db/scheduleStoreContractSuite.ts | 1 + .../trueforge/tests/unit/apis/agents.test.ts | 21 +++++++++- .../tests/unit/apis/schedules.test.ts | 2 + .../tests/unit/apis/sessionHttp.test.ts | 5 +++ .../trueforge/tests/unit/apis/turns.test.ts | 2 + .../truefoundry/TrueFoundryAgentStore.test.ts | 35 +++++++++++++++- .../unit/truefoundry/accessToken.test.ts | 1 + 21 files changed, 189 insertions(+), 23 deletions(-) create mode 100644 .changeset/agent-description.md create mode 100644 packages/trueforge/src/db/postgres/migrations/20260909_000001_agent_description.ts create mode 100644 packages/trueforge/src/db/sqlite/migrations/20260909_000001_agent_description.ts diff --git a/.changeset/agent-description.md b/.changeset/agent-description.md new file mode 100644 index 000000000..59884d1b6 --- /dev/null +++ b/.changeset/agent-description.md @@ -0,0 +1,5 @@ +--- +"@truefoundry/trueforge": patch +--- + +Persist top-level agent description and sync it to ServiceFoundry on create/update. diff --git a/packages/trueforge/src/apis/agents.ts b/packages/trueforge/src/apis/agents.ts index 3661d7747..04c6df4a5 100644 --- a/packages/trueforge/src/apis/agents.ts +++ b/packages/trueforge/src/apis/agents.ts @@ -46,6 +46,7 @@ function toWireAgent(record: AgentRecord): Agent { return { id: record.id, name: record.name, + description: record.description, manifest: record.manifest, created_by_subject: record.created_by_subject, }; @@ -104,6 +105,7 @@ export function createAgentsRouter(deps: AgentsRouterDeps(deps: AgentsRouterDeps { getAgent(input: GetAgentInput, transaction?: TTransaction): Promise; /** Inserts a new agent with a generated ULID. Throws AgentNameConflictError or AgentExternalIdConflictError on unique clash. */ createAgent(input: CreateAgentInput, transaction?: TTransaction): Promise; - /** Patches `manifest` and/or `external_id`. Throws AgentExternalIdConflictError on unique clash. Returns undefined if missing. */ + /** Patches `manifest`, `description`, and/or `external_id`. Throws AgentExternalIdConflictError on unique clash. Returns undefined if missing. */ updateAgent(input: UpdateAgentInput, transaction?: TTransaction): Promise; /** Deletes by immutable id. Idempotent if already missing. */ deleteAgent(input: DeleteAgentInput, transaction?: TTransaction): Promise; diff --git a/packages/trueforge/src/db/postgres/agent-store/PostgresAgentStore.ts b/packages/trueforge/src/db/postgres/agent-store/PostgresAgentStore.ts index 38f68fded..01f486d84 100644 --- a/packages/trueforge/src/db/postgres/agent-store/PostgresAgentStore.ts +++ b/packages/trueforge/src/db/postgres/agent-store/PostgresAgentStore.ts @@ -23,6 +23,7 @@ function toRecord(row: Selectable): AgentRecord { id: row.id, tenant_id: row.tenant_id, name: row.name, + description: row.description, manifest: parseStoredAgentSpec(row.manifest), external_id: row.external_id, created_by_subject: CreatedBySubjectSchema.parse(row.created_by_subject), @@ -90,6 +91,7 @@ export class PostgresAgentStore implements IAgentStore> { id: newId(), tenant_id: input.tenant_id, name: input.name, + description: input.description, manifest: json(input.manifest), external_id: input.external_id, created_by_subject: json(input.created_by_subject), @@ -113,8 +115,8 @@ export class PostgresAgentStore implements IAgentStore> { } async updateAgent(input: UpdateAgentInput, transaction?: Transaction): Promise { - if (input.manifest === undefined && input.external_id === undefined) { - throw new Error('updateAgent requires manifest and/or external_id'); + if (input.manifest === undefined && input.description === undefined && input.external_id === undefined) { + throw new Error('updateAgent requires manifest, description, and/or external_id'); } const db = transaction ?? this.#db; try { @@ -122,6 +124,7 @@ export class PostgresAgentStore implements IAgentStore> { .updateTable('agent') .set({ ...(input.manifest === undefined ? {} : { manifest: json(input.manifest) }), + ...(input.description === undefined ? {} : { description: input.description }), ...(input.external_id === undefined ? {} : { external_id: input.external_id }), updated_at: now(), }) diff --git a/packages/trueforge/src/db/postgres/migrations/20260909_000001_agent_description.ts b/packages/trueforge/src/db/postgres/migrations/20260909_000001_agent_description.ts new file mode 100644 index 000000000..9e3bd1f4a --- /dev/null +++ b/packages/trueforge/src/db/postgres/migrations/20260909_000001_agent_description.ts @@ -0,0 +1,16 @@ +import { sql, type Kysely } from 'kysely'; + +/** Add agent `description`; backfill existing rows from `name`. */ +export async function up(db: Kysely): Promise { + await sql`SET LOCAL lock_timeout = '5s'`.execute(db); + await db.schema + .alterTable('agent') + .addColumn('description', 'text', col => col.notNull().defaultTo('')) + .execute(); + await sql`UPDATE agent SET description = name`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`SET LOCAL lock_timeout = '5s'`.execute(db); + await db.schema.alterTable('agent').dropColumn('description').execute(); +} diff --git a/packages/trueforge/src/db/postgres/types.ts b/packages/trueforge/src/db/postgres/types.ts index 17475d47e..15d0f810d 100644 --- a/packages/trueforge/src/db/postgres/types.ts +++ b/packages/trueforge/src/db/postgres/types.ts @@ -379,6 +379,7 @@ export interface AgentTable { tenant_id: string; /** immutable natural uniqueness target within a tenant */ name: string; + description: string; /** AgentSpec document; replaced whole on every upsert */ manifest: JSONColumnType; external_id: string | null; diff --git a/packages/trueforge/src/db/sqlite/agent-store/SqliteAgentStore.ts b/packages/trueforge/src/db/sqlite/agent-store/SqliteAgentStore.ts index e4bd2463c..60da7a396 100644 --- a/packages/trueforge/src/db/sqlite/agent-store/SqliteAgentStore.ts +++ b/packages/trueforge/src/db/sqlite/agent-store/SqliteAgentStore.ts @@ -27,6 +27,7 @@ function recordColumns(eb: ExpressionBuilder) { 'id' as const, 'tenant_id' as const, 'name' as const, + 'description' as const, jsonText(eb.ref('manifest')).as('manifest'), 'external_id' as const, jsonText(eb.ref('created_by_subject')).as('created_by_subject'), @@ -39,6 +40,7 @@ function toRecord(row: { id: string; tenant_id: string; name: AgentRecord['name']; + description: string; manifest: AgentSpec; external_id: string | null; created_by_subject: CreatedBySubject; @@ -94,6 +96,7 @@ export class SqliteAgentStore implements IAgentStore> { id: newId(), tenant_id: input.tenant_id, name: input.name, + description: input.description, manifest: jsonbBind(input.manifest), external_id: input.external_id, created_by_subject: jsonbBind(input.created_by_subject), @@ -118,8 +121,8 @@ export class SqliteAgentStore implements IAgentStore> { } async updateAgent(input: UpdateAgentInput, transaction?: Transaction): Promise { - if (input.manifest === undefined && input.external_id === undefined) { - throw new Error('updateAgent requires manifest and/or external_id'); + if (input.manifest === undefined && input.description === undefined && input.external_id === undefined) { + throw new Error('updateAgent requires manifest, description, and/or external_id'); } const db = transaction ?? this.#db; try { @@ -127,6 +130,7 @@ export class SqliteAgentStore implements IAgentStore> { .updateTable('agent') .set({ ...(input.manifest === undefined ? {} : { manifest: jsonbBind(input.manifest) }), + ...(input.description === undefined ? {} : { description: input.description }), ...(input.external_id === undefined ? {} : { external_id: input.external_id }), updated_at: nowIso(), }) diff --git a/packages/trueforge/src/db/sqlite/migrations/20260909_000001_agent_description.ts b/packages/trueforge/src/db/sqlite/migrations/20260909_000001_agent_description.ts new file mode 100644 index 000000000..98cd85860 --- /dev/null +++ b/packages/trueforge/src/db/sqlite/migrations/20260909_000001_agent_description.ts @@ -0,0 +1,15 @@ +import { type Kysely, sql } from 'kysely'; + +/** Add agent `description`; backfill existing rows from `name`. Mirrors postgres. */ +export async function up(db: Kysely): Promise { + await db.transaction().execute(async trx => { + await sql`ALTER TABLE agent ADD COLUMN description TEXT NOT NULL DEFAULT ''`.execute(trx); + await sql`UPDATE agent SET description = name`.execute(trx); + }); +} + +export async function down(db: Kysely): Promise { + await db.transaction().execute(async trx => { + await sql`ALTER TABLE agent DROP COLUMN description`.execute(trx); + }); +} diff --git a/packages/trueforge/src/db/sqlite/types.ts b/packages/trueforge/src/db/sqlite/types.ts index 476047b5d..aade0d563 100644 --- a/packages/trueforge/src/db/sqlite/types.ts +++ b/packages/trueforge/src/db/sqlite/types.ts @@ -223,6 +223,7 @@ export interface AgentTable { tenant_id: string; /** natural uniqueness target within a tenant */ name: string; + description: string; /** AgentSpec document; replaced whole on every upsert */ manifest: JsonbColumn; external_id: string | null; diff --git a/packages/trueforge/src/routes/agentRoutes.ts b/packages/trueforge/src/routes/agentRoutes.ts index 113463292..3a5f68558 100644 --- a/packages/trueforge/src/routes/agentRoutes.ts +++ b/packages/trueforge/src/routes/agentRoutes.ts @@ -154,7 +154,7 @@ export const putAgentRoute = createRoute({ path: '/{agent_id}', tags: [OpenApiTag.AGENTS], summary: 'Update an agent', - description: 'Replaces the manifest for an existing agent keyed by immutable `agent_id`.', + description: 'Update an existing agent by immutable id.', 'x-fern-sdk-group-name': ['agents'], 'x-fern-sdk-method-name': 'update', request: { diff --git a/packages/trueforge/src/schemas/agent.ts b/packages/trueforge/src/schemas/agent.ts index 8dc3f0068..774f2e1df 100644 --- a/packages/trueforge/src/schemas/agent.ts +++ b/packages/trueforge/src/schemas/agent.ts @@ -8,20 +8,30 @@ import { NameSchema } from './common'; const RESERVED_AGENT_NAMES = new Set(['tfg', 'trueforge']); +export const AGENT_DESCRIPTION_MAX_LENGTH = 1024; + +export const AgentDescriptionSchema = z + .string() + .trim() + .max(AGENT_DESCRIPTION_MAX_LENGTH) + .describe('Short summary of what the agent does.'); + /** Create body: unique immutable `name` plus manifest. `id` is never client-supplied. */ export const CreateAgentRequestSchema = z .object({ name: NameSchema.refine(name => !RESERVED_AGENT_NAMES.has(name), { message: 'Agent name is reserved, cannot be used', }), + description: AgentDescriptionSchema.default(''), manifest: AgentSpecSchema, }) .strict() .openapi('CreateAgentRequest'); -/** PUT body: full manifest replacement only. */ +/** PUT body: full manifest replacement; `description` optional. */ export const UpdateAgentRequestSchema = z .object({ + description: AgentDescriptionSchema.optional(), manifest: AgentSpecSchema, }) .strict() @@ -32,12 +42,12 @@ export const AgentSchema = z .object({ id: z.string().min(1).describe('Immutable server-generated agent identifier.'), name: NameSchema, + description: AgentDescriptionSchema, manifest: AgentSpecSchema, created_by_subject: CreatedBySubjectSchema, }) .strict() .openapi('Agent'); - export const GetAgentResponseSchema = z.object({ data: AgentSchema }).openapi('GetAgentResponse'); export const ListAgentsResponseSchema = z.object({ data: z.array(AgentSchema) }).openapi('ListAgentsResponse'); export const DeleteAgentResponseSchema = z.object({}).openapi('DeleteAgentResponse'); diff --git a/packages/trueforge/src/truefoundry/TrueFoundryAgentStore.ts b/packages/trueforge/src/truefoundry/TrueFoundryAgentStore.ts index c20266f8a..73458ddc3 100644 --- a/packages/trueforge/src/truefoundry/TrueFoundryAgentStore.ts +++ b/packages/trueforge/src/truefoundry/TrueFoundryAgentStore.ts @@ -12,6 +12,7 @@ import { } from '../db/agentStore'; import { PostgresAgentStore } from '../db/postgres/agent-store/PostgresAgentStore'; import type { Database } from '../db/postgres/types'; +import { AGENT_DESCRIPTION_MAX_LENGTH } from '../schemas/agent'; import { callerAccessToken, type ResolveAccessToken } from './accessToken'; import { TrueFoundryServiceFoundryServerClient, @@ -24,14 +25,16 @@ function asError(value: unknown): Error { function toPutRemoteAgentPayload({ name, + description, manifest, }: { name: string; + description: string; manifest: AgentSpec; }): Omit { return { name, - description: name, + description: (description || name).slice(0, AGENT_DESCRIPTION_MAX_LENGTH), model: manifest.model.name, mcp_servers: (manifest.mcp_servers ?? []).map(server => server.name), }; @@ -52,11 +55,11 @@ function toPutRemoteAgentPayload({ * update(external_id) fails after put: delete remote (if put returned an id), then * delete local; if cleanup also fails, AggregateError (primary + cleanup errors). * - * update (manifest) - * Happy path: lock → load row → put remote (new manifest) → write local. + * update (manifest and/or description) + * Happy path: lock → load row → put remote (new values) → write local. * Missing row: return undefined (no remote call). * putRemote fails: leave local unchanged; rethrow. - * local write fails after put: best-effort putRemote(old manifest); if restore fails, + * local write fails after put: best-effort putRemote(old values); if restore fails, * AggregateError; if restore ok, rethrow the DB error (local still old, remote restored). * external_id-only patches skip ServiceFoundry and go straight to the inner store. * @@ -117,7 +120,11 @@ export class TrueFoundryAgentStore implements IAgentStore> try { ({ externalId } = await this.#client.putRemoteAgent({ accessToken: await this.#resolveAccessToken(), - ...toPutRemoteAgentPayload({ name: input.name, manifest: input.manifest }), + ...toPutRemoteAgentPayload({ + name: input.name, + description: input.description, + manifest: input.manifest, + }), })); const updated = await this.#inner.updateAgent( { tenant_id: input.tenant_id, id: created.id, external_id: externalId }, @@ -149,9 +156,8 @@ export class TrueFoundryAgentStore implements IAgentStore> } async updateAgent(input: UpdateAgentInput, transaction?: Transaction): Promise { - const nextManifest = input.manifest; - if (nextManifest === undefined) { - // No manifest means only `external_id` changed; pass through to the inner store. + if (input.manifest === undefined && input.description === undefined) { + // No manifest/description means only `external_id` changed; pass through to the inner store. return this.#inner.updateAgent(input, transaction); } @@ -161,9 +167,16 @@ export class TrueFoundryAgentStore implements IAgentStore> return undefined; } + const nextManifest = input.manifest ?? previous.manifest; + const nextDescription = input.description ?? previous.description; + const { externalId } = await this.#client.putRemoteAgent({ accessToken: await this.#resolveAccessToken(), - ...toPutRemoteAgentPayload({ name: previous.name, manifest: nextManifest }), + ...toPutRemoteAgentPayload({ + name: previous.name, + description: nextDescription, + manifest: nextManifest, + }), }); try { @@ -171,7 +184,8 @@ export class TrueFoundryAgentStore implements IAgentStore> { tenant_id: input.tenant_id, id: input.id, - manifest: nextManifest, + ...(input.manifest === undefined ? {} : { manifest: nextManifest }), + ...(input.description === undefined ? {} : { description: nextDescription }), ...(externalId === previous.external_id ? {} : { external_id: externalId }), }, txn, @@ -180,7 +194,11 @@ export class TrueFoundryAgentStore implements IAgentStore> try { await this.#client.putRemoteAgent({ accessToken: await this.#resolveAccessToken(), - ...toPutRemoteAgentPayload({ name: previous.name, manifest: previous.manifest }), + ...toPutRemoteAgentPayload({ + name: previous.name, + description: previous.description, + manifest: previous.manifest, + }), }); } catch (restoreError) { throw new AggregateError( diff --git a/packages/trueforge/tests/db/agentStoreContractSuite.ts b/packages/trueforge/tests/db/agentStoreContractSuite.ts index 2d26f7693..246e12c07 100644 --- a/packages/trueforge/tests/db/agentStoreContractSuite.ts +++ b/packages/trueforge/tests/db/agentStoreContractSuite.ts @@ -30,12 +30,14 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'research', + description: 'Research helper.', manifest: manifest(), external_id: null, }); expect(created.tenant_id).toBe(TENANT); expect(created.name).toBe('research'); + expect(created.description).toBe('Research helper.'); expect(created.id.length).toBeGreaterThan(0); expect(created.manifest).toEqual(manifest()); expect(created.external_id).toBeNull(); @@ -62,6 +64,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'research', + description: 'Research helper.', manifest: manifest(), external_id: null, }); @@ -70,6 +73,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { const updated = await store.updateAgent({ tenant_id: TENANT, id: created.id, + description: 'Updated research helper.', manifest: replacement, }); @@ -77,6 +81,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { expect.objectContaining({ id: created.id, name: 'research', + description: 'Updated research helper.', manifest: replacement, created_at: created.created_at, }), @@ -107,6 +112,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'research', + description: '', manifest: manifest(), external_id: null, }); @@ -116,6 +122,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'research', + description: '', manifest: manifest(), external_id: null, }), @@ -128,6 +135,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'zeta', + description: '', manifest: manifest(), external_id: null, }); @@ -135,6 +143,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'alpha', + description: '', manifest: manifest({ instructions: 'Alpha agent.' }), external_id: null, }); @@ -142,6 +151,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: 'other-tenant', created_by_subject: CREATED_BY_SUBJECT, name: 'research', + description: '', manifest: manifest(), external_id: null, }); @@ -157,6 +167,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'local-only', + description: '', manifest: manifest(), external_id: null, }); @@ -164,6 +175,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'linked', + description: '', manifest: manifest(), external_id: 'sf-agent-1', }); @@ -171,6 +183,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'other-linked', + description: '', manifest: manifest(), external_id: 'sf-agent-2', }); @@ -191,6 +204,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'research', + description: '', manifest: manifest(), external_id: null, }); @@ -204,6 +218,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'research', + description: '', manifest: manifest(), external_id: 'sf-agent-1', }); @@ -217,6 +232,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'alpha', + description: '', manifest: manifest(), external_id: 'shared-key', }); @@ -225,6 +241,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'beta', + description: '', manifest: manifest(), external_id: 'shared-key', }), @@ -233,6 +250,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: 'other-tenant', created_by_subject: CREATED_BY_SUBJECT, name: 'alpha', + description: '', manifest: manifest(), external_id: 'shared-key', }); @@ -240,6 +258,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'gamma', + description: '', manifest: manifest(), external_id: null, }); @@ -247,6 +266,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'delta', + description: '', manifest: manifest(), external_id: null, }); @@ -258,6 +278,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'research', + description: '', manifest: manifest(), external_id: null, }); @@ -281,6 +302,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'alpha', + description: '', manifest: manifest(), external_id: 'shared-key', }); @@ -288,6 +310,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'beta', + description: '', manifest: manifest(), external_id: null, }); diff --git a/packages/trueforge/tests/db/scheduleDispatchContractSuite.ts b/packages/trueforge/tests/db/scheduleDispatchContractSuite.ts index 4d87c2df2..7f704fb6d 100644 --- a/packages/trueforge/tests/db/scheduleDispatchContractSuite.ts +++ b/packages/trueforge/tests/db/scheduleDispatchContractSuite.ts @@ -71,6 +71,7 @@ export function runScheduleDispatchContractSuite(deps: { tenant_id: TENANT, created_by_subject: USER_SUBJECT, name: `agent-${String(Date.now())}-${String(seq)}`, + description: '', manifest: AgentSpecSchema.parse({ model: { name: 'anthropic/claude-sonnet-4-6' }, instructions: 'Be helpful.', diff --git a/packages/trueforge/tests/db/scheduleStoreContractSuite.ts b/packages/trueforge/tests/db/scheduleStoreContractSuite.ts index 707873f7e..c38443021 100644 --- a/packages/trueforge/tests/db/scheduleStoreContractSuite.ts +++ b/packages/trueforge/tests/db/scheduleStoreContractSuite.ts @@ -31,6 +31,7 @@ export function runScheduleStoreContractSuite(deps: { tenant_id: TENANT, created_by_subject: USER_SUBJECT, name: `agent-${String(Date.now())}-${String(Math.random()).slice(2, 8)}`, + description: '', manifest: AgentSpecSchema.parse({ model: { name: 'anthropic/claude-sonnet-4-6' }, instructions: 'Be helpful.', diff --git a/packages/trueforge/tests/unit/apis/agents.test.ts b/packages/trueforge/tests/unit/apis/agents.test.ts index 795ce5a12..0d83a57fc 100644 --- a/packages/trueforge/tests/unit/apis/agents.test.ts +++ b/packages/trueforge/tests/unit/apis/agents.test.ts @@ -41,6 +41,7 @@ const writeBody = { }; const updateBody = { + description: 'Updated research agent.', manifest: { model: { name: 'anthropic/claude-sonnet-4-6' }, instructions: 'Updated instructions.', @@ -50,6 +51,7 @@ const updateBody = { type WireAgent = { id: string; name: string; + description: string; manifest: { model: { name: string }; instructions?: string; @@ -112,12 +114,13 @@ describe('agents router', () => { }); it('POST returns a wrapped Agent; PUT by immutable id keeps the same id', async () => { - const created = await router.request('/', jsonInit('POST', writeBody)); + const created = await router.request('/', jsonInit('POST', { ...writeBody, description: ' Research helper. ' })); expect(created.status).toBe(201); const createdJson = (await created.json()) as { data: WireAgent }; expect(createdJson.data.id.length).toBeGreaterThan(0); expect(createdJson.data).toMatchObject({ name: 'research', + description: 'Research helper.', manifest: { model: { name: 'anthropic/claude-sonnet-4-6' }, instructions: 'Be helpful.', @@ -138,10 +141,26 @@ describe('agents router', () => { const updatedJson = (await updated.json()) as { data: WireAgent }; expect(updatedJson.data.id).toBe(createdJson.data.id); expect(updatedJson.data.name).toBe('research'); + expect(updatedJson.data.description).toBe('Updated research agent.'); expect(updatedJson.data.manifest.instructions).toBe('Updated instructions.'); expect(updatedJson.data).not.toHaveProperty('metadata'); }); + it('PUT with only manifest keeps the stored description', async () => { + const created = await router.request( + '/', + jsonInit('POST', { ...writeBody, name: 'keep-desc', description: 'Keep me.' }), + ); + expect(created.status).toBe(201); + const createdJson = (await created.json()) as { data: WireAgent }; + + const updated = await router.request(`/${createdJson.data.id}`, jsonInit('PUT', { manifest: updateBody.manifest })); + expect(updated.status).toBe(200); + const updatedJson = (await updated.json()) as { data: WireAgent }; + expect(updatedJson.data.description).toBe('Keep me.'); + expect(updatedJson.data.manifest.instructions).toBe('Updated instructions.'); + }); + it('PUT rejects metadata in the request body', async () => { const created = await router.request('/', jsonInit('POST', { ...writeBody, name: 'no-meta' })); expect(created.status).toBe(201); diff --git a/packages/trueforge/tests/unit/apis/schedules.test.ts b/packages/trueforge/tests/unit/apis/schedules.test.ts index 3c0137a28..2a12c4f66 100644 --- a/packages/trueforge/tests/unit/apis/schedules.test.ts +++ b/packages/trueforge/tests/unit/apis/schedules.test.ts @@ -61,6 +61,7 @@ async function setup(authorizer: Authorizer = new TrueForgeAuthorizer()) { subject_display_name: 'alice', }, name: 'reporter', + description: '', manifest: AgentSpecSchema.parse({ model: { name: 'test-provider/test-model' }, instructions: 'test' }), external_id: 'reporter-external-id', }); @@ -220,6 +221,7 @@ describe('schedule list agent_names filter', () => { subject_display_name: 'alice', }, name: 'reporter-two', + description: '', manifest: AgentSpecSchema.parse({ model: { name: 'test-provider/test-model' }, instructions: 'test' }), external_id: null, }); diff --git a/packages/trueforge/tests/unit/apis/sessionHttp.test.ts b/packages/trueforge/tests/unit/apis/sessionHttp.test.ts index 3216b79a8..1b34e363d 100644 --- a/packages/trueforge/tests/unit/apis/sessionHttp.test.ts +++ b/packages/trueforge/tests/unit/apis/sessionHttp.test.ts @@ -152,6 +152,7 @@ describe('sessions HTTP agent binding', () => { subject_display_name: STANDALONE_REQUEST_CONTEXT.subject.display_name, }, name: 'named-agent', + description: '', manifest: AgentSpecSchema.parse({ model: { name: 'anthropic/claude-sonnet-4-6' }, instructions: 'from-registry', @@ -184,6 +185,7 @@ describe('sessions HTTP agent binding', () => { subject_display_name: STANDALONE_REQUEST_CONTEXT.subject.display_name, }, name: 'metrics-agent', + description: '', manifest: inlineSpec, external_id: null, }); @@ -243,6 +245,7 @@ describe('sessions HTTP agent binding', () => { tenant_id: 'default', created_by_subject: { subject_id: 'owner', subject_type: 'user', subject_display_name: 'Owner' }, name: 'managed-agent', + description: '', manifest: inlineSpec, external_id: 'managed-agent-external', }); @@ -422,6 +425,7 @@ describe('sessions HTTP agent binding', () => { subject_display_name: STANDALONE_REQUEST_CONTEXT.subject.display_name, }, name: 'named-agent', + description: '', manifest: AgentSpecSchema.parse({ model: { name: 'anthropic/claude-sonnet-4-6' }, instructions: 'from-registry', @@ -603,6 +607,7 @@ describe('sessions HTTP agent binding', () => { subject_display_name: STANDALONE_REQUEST_CONTEXT.subject.display_name, }, name: 'forbidden-agent', + description: '', manifest: inlineSpec, external_id: null, }); diff --git a/packages/trueforge/tests/unit/apis/turns.test.ts b/packages/trueforge/tests/unit/apis/turns.test.ts index 0aea4dae3..0ef370798 100644 --- a/packages/trueforge/tests/unit/apis/turns.test.ts +++ b/packages/trueforge/tests/unit/apis/turns.test.ts @@ -120,6 +120,7 @@ describe('turns', () => { const agent = await agentStore.createAgent({ tenant_id: 'default', name: 'managed-agent', + description: '', manifest: AgentSpecSchema.parse({ model: { name: 'test-provider/test-model' } }), external_id: 'managed-agent-external', created_by_subject: { subject_id: 'owner', subject_type: 'user', subject_display_name: 'Owner' }, @@ -427,6 +428,7 @@ describe('turns', () => { subject_display_name: STANDALONE_REQUEST_CONTEXT.subject.display_name, }, name: 'named-for-turn', + description: '', manifest: AgentSpecSchema.parse({ model: { name: 'test-provider/test-model' }, instructions: 'test', diff --git a/packages/trueforge/tests/unit/truefoundry/TrueFoundryAgentStore.test.ts b/packages/trueforge/tests/unit/truefoundry/TrueFoundryAgentStore.test.ts index 50028fb48..5521dc6bd 100644 --- a/packages/trueforge/tests/unit/truefoundry/TrueFoundryAgentStore.test.ts +++ b/packages/trueforge/tests/unit/truefoundry/TrueFoundryAgentStore.test.ts @@ -59,6 +59,7 @@ function record(overrides: Partial = {}): AgentRecord { id: 'agent-1', tenant_id: TENANT, name: 'research', + description: '', manifest: manifest(), external_id: null, created_by_subject: CREATED_BY_SUBJECT, @@ -178,6 +179,7 @@ describe('TrueFoundryAgentStore', () => { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'research', + description: '', manifest: manifest({ mcp_servers: [{ name: 'slack' }] }), external_id: null, }, @@ -215,6 +217,7 @@ describe('TrueFoundryAgentStore', () => { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'research', + description: '', manifest: manifest(), external_id: null, }, @@ -226,7 +229,7 @@ describe('TrueFoundryAgentStore', () => { expect(deleteRemoteAgent).not.toHaveBeenCalled(); }); - it('createAgent uses agent name as description even when instructions are empty', async () => { + it('createAgent uses agent name as description when description is blank', async () => { const putRemoteAgent = jest.fn(async (input: PutRemoteAgentInput) => { expect(input.description).toBe('research'); expect(input.mcp_servers).toEqual([]); @@ -244,6 +247,7 @@ describe('TrueFoundryAgentStore', () => { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'research', + description: '', manifest: AgentSpecSchema.parse({ model: { name: 'openai-gateway/gpt-5' }, instructions: '' }), external_id: null, }, @@ -252,6 +256,32 @@ describe('TrueFoundryAgentStore', () => { expect(putRemoteAgent).toHaveBeenCalled(); }); + it('createAgent syncs description to ServiceFoundry', async () => { + const putRemoteAgent = jest.fn(async (input: PutRemoteAgentInput) => { + expect(input.description).toBe('Finds papers'); + return { externalId: 'sf-1' }; + }); + const createAgent = jest.fn(async () => record({ external_id: null, description: 'Finds papers' })); + const updateAgent = jest.fn(async () => record({ external_id: 'sf-1', description: 'Finds papers' })); + const store = tfStore({ + inner: mockInner({ createAgent, updateAgent }), + client: mockClient({ putRemoteAgent }), + }); + + await store.createAgent( + { + tenant_id: TENANT, + created_by_subject: CREATED_BY_SUBJECT, + name: 'research', + description: 'Finds papers', + manifest: manifest(), + external_id: null, + }, + TXN, + ); + expect(putRemoteAgent).toHaveBeenCalled(); + }); + it('createAgent deletes the local row when putRemoteAgent fails', async () => { const local = record({ external_id: null }); const createAgent = jest.fn(async () => local); @@ -272,6 +302,7 @@ describe('TrueFoundryAgentStore', () => { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'research', + description: '', manifest: manifest(), external_id: null, }, @@ -302,6 +333,7 @@ describe('TrueFoundryAgentStore', () => { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'research', + description: '', manifest: manifest(), external_id: null, }, @@ -335,6 +367,7 @@ describe('TrueFoundryAgentStore', () => { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'research', + description: '', manifest: manifest(), external_id: null, }, diff --git a/packages/trueforge/tests/unit/truefoundry/accessToken.test.ts b/packages/trueforge/tests/unit/truefoundry/accessToken.test.ts index 570090f73..df1bb59eb 100644 --- a/packages/trueforge/tests/unit/truefoundry/accessToken.test.ts +++ b/packages/trueforge/tests/unit/truefoundry/accessToken.test.ts @@ -22,6 +22,7 @@ const AGENT: AgentRecord = { id: 'agent-1', tenant_id: 'acme', name: 'named', + description: '', manifest: AgentSpecSchema.parse({ model: { name: 'p/m' } }), external_id: 'ext-agent', created_by_subject: { subject_id: 'user-1', subject_type: 'user', subject_display_name: 'User' }, From a29bca946cfbba822634b8bacac02b93ccb30d16 Mon Sep 17 00:00:00 2001 From: Bhavesh Patel Date: Fri, 11 Sep 2026 12:27:28 +0530 Subject: [PATCH 2/8] fixes --- packages/trueforge/src/apis/agentImport.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/trueforge/src/apis/agentImport.ts b/packages/trueforge/src/apis/agentImport.ts index 24e144f75..a2cb00f8c 100644 --- a/packages/trueforge/src/apis/agentImport.ts +++ b/packages/trueforge/src/apis/agentImport.ts @@ -46,6 +46,7 @@ export function createAgentImportRouter(deps: AgentImportRouterDeps) { const created = await agentStore.createAgent({ tenant_id: agent.tenant_id, name: agent.name, + description: agent.name, manifest: agent.manifest, external_id: null, created_by_subject: agent.created_by_subject, From 1e00dcaf8acd35feea518893e23f0693c0e799c9 Mon Sep 17 00:00:00 2001 From: "trueforge-dev-bot[bot]" Date: Fri, 11 Sep 2026 06:59:47 +0000 Subject: [PATCH 3/8] Regenerate OpenAPI document and TypeScript SDK --- ...60911065947-regenerate-sdk-from-openapi.md | 5 +++++ .github/fern/openapi/openapi.json | 21 +++++++++++++++++-- docs/openapi.json | 21 +++++++++++++++++-- packages/trueforge-sdk/reference.md | 2 +- .../src/api/resources/agents/client/Client.ts | 2 +- .../client/requests/CreateAgentRequest.ts | 2 ++ .../client/requests/UpdateAgentRequest.ts | 2 ++ packages/trueforge-sdk/src/api/types/Agent.ts | 2 ++ .../client/requests/CreateAgentRequest.ts | 2 ++ .../client/requests/UpdateAgentRequest.ts | 2 ++ .../src/serialization/types/Agent.ts | 2 ++ .../trueforge-sdk/tests/wire/agents.test.ts | 8 +++++++ 12 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 .changeset/20260911065947-regenerate-sdk-from-openapi.md diff --git a/.changeset/20260911065947-regenerate-sdk-from-openapi.md b/.changeset/20260911065947-regenerate-sdk-from-openapi.md new file mode 100644 index 000000000..efd8ff00f --- /dev/null +++ b/.changeset/20260911065947-regenerate-sdk-from-openapi.md @@ -0,0 +1,5 @@ +--- +"@truefoundry/trueforge-sdk": patch +--- + +Regenerate SDK from updated OpenAPI spec. diff --git a/.github/fern/openapi/openapi.json b/.github/fern/openapi/openapi.json index c494b6827..b295b3484 100644 --- a/.github/fern/openapi/openapi.json +++ b/.github/fern/openapi/openapi.json @@ -29,6 +29,11 @@ "created_by_subject": { "$ref": "#/components/schemas/CreatedBySubject" }, + "description": { + "description": "Short summary of what the agent does.", + "maxLength": 1024, + "type": "string" + }, "id": { "description": "Immutable server-generated agent identifier.", "minLength": 1, @@ -44,6 +49,7 @@ "required": [ "id", "name", + "description", "manifest", "created_by_subject" ], @@ -982,6 +988,12 @@ "CreateAgentRequest": { "additionalProperties": false, "properties": { + "description": { + "default": "", + "description": "Short summary of what the agent does.", + "maxLength": 1024, + "type": "string" + }, "manifest": { "$ref": "#/components/schemas/AgentSpec" }, @@ -5326,6 +5338,11 @@ "UpdateAgentRequest": { "additionalProperties": false, "properties": { + "description": { + "description": "Short summary of what the agent does.", + "maxLength": 1024, + "type": "string" + }, "manifest": { "$ref": "#/components/schemas/AgentSpec" } @@ -5569,7 +5586,7 @@ "info": { "description": "HTTP API for the TrueForge agent server (`/api/v1`). Interactive docs are served at `/api/v1/docs` (OpenAPI JSON at `/api/v1/openapi.json`).\n\n**Authentication:** Standalone auth accepts requests without credentials — middleware stamps a local default user. When OIDC or TrueFoundry auth is configured, protected routes require a valid cookie or `Authorization: Bearer` token. There is no built-in API-key scheme; pass custom headers only if your reverse proxy or IdP layer requires them.\n\nCovers DB-backed sessions, the agent registry, settings catalogs, and model/MCP/skill/sandbox providers.", "title": "TrueForge API", - "version": "0.2.0-rc.4" + "version": "0.2.0-rc.5" }, "openapi": "3.1.0", "paths": { @@ -6315,7 +6332,7 @@ "x-fern-sdk-method-name": "get" }, "put": { - "description": "Replaces the manifest for an existing agent keyed by immutable `agent_id`.", + "description": "Update an existing agent by immutable id.", "parameters": [ { "description": "Immutable agent identifier.", diff --git a/docs/openapi.json b/docs/openapi.json index c494b6827..b295b3484 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -29,6 +29,11 @@ "created_by_subject": { "$ref": "#/components/schemas/CreatedBySubject" }, + "description": { + "description": "Short summary of what the agent does.", + "maxLength": 1024, + "type": "string" + }, "id": { "description": "Immutable server-generated agent identifier.", "minLength": 1, @@ -44,6 +49,7 @@ "required": [ "id", "name", + "description", "manifest", "created_by_subject" ], @@ -982,6 +988,12 @@ "CreateAgentRequest": { "additionalProperties": false, "properties": { + "description": { + "default": "", + "description": "Short summary of what the agent does.", + "maxLength": 1024, + "type": "string" + }, "manifest": { "$ref": "#/components/schemas/AgentSpec" }, @@ -5326,6 +5338,11 @@ "UpdateAgentRequest": { "additionalProperties": false, "properties": { + "description": { + "description": "Short summary of what the agent does.", + "maxLength": 1024, + "type": "string" + }, "manifest": { "$ref": "#/components/schemas/AgentSpec" } @@ -5569,7 +5586,7 @@ "info": { "description": "HTTP API for the TrueForge agent server (`/api/v1`). Interactive docs are served at `/api/v1/docs` (OpenAPI JSON at `/api/v1/openapi.json`).\n\n**Authentication:** Standalone auth accepts requests without credentials — middleware stamps a local default user. When OIDC or TrueFoundry auth is configured, protected routes require a valid cookie or `Authorization: Bearer` token. There is no built-in API-key scheme; pass custom headers only if your reverse proxy or IdP layer requires them.\n\nCovers DB-backed sessions, the agent registry, settings catalogs, and model/MCP/skill/sandbox providers.", "title": "TrueForge API", - "version": "0.2.0-rc.4" + "version": "0.2.0-rc.5" }, "openapi": "3.1.0", "paths": { @@ -6315,7 +6332,7 @@ "x-fern-sdk-method-name": "get" }, "put": { - "description": "Replaces the manifest for an existing agent keyed by immutable `agent_id`.", + "description": "Update an existing agent by immutable id.", "parameters": [ { "description": "Immutable agent identifier.", diff --git a/packages/trueforge-sdk/reference.md b/packages/trueforge-sdk/reference.md index 31ba5882c..dd0e0a9aa 100644 --- a/packages/trueforge-sdk/reference.md +++ b/packages/trueforge-sdk/reference.md @@ -267,7 +267,7 @@ await client.agents.get("agent_id");
-Replaces the manifest for an existing agent keyed by immutable `agent_id`. +Update an existing agent by immutable id.
diff --git a/packages/trueforge-sdk/src/api/resources/agents/client/Client.ts b/packages/trueforge-sdk/src/api/resources/agents/client/Client.ts index fbf595c8c..fa1440f6f 100644 --- a/packages/trueforge-sdk/src/api/resources/agents/client/Client.ts +++ b/packages/trueforge-sdk/src/api/resources/agents/client/Client.ts @@ -310,7 +310,7 @@ export class AgentsClient { } /** - * Replaces the manifest for an existing agent keyed by immutable `agent_id`. + * Update an existing agent by immutable id. * * @param {string} agent_id - Immutable agent identifier. * @param {TrueForge.UpdateAgentRequest} request diff --git a/packages/trueforge-sdk/src/api/resources/agents/client/requests/CreateAgentRequest.ts b/packages/trueforge-sdk/src/api/resources/agents/client/requests/CreateAgentRequest.ts index 0648583e5..42f88b580 100644 --- a/packages/trueforge-sdk/src/api/resources/agents/client/requests/CreateAgentRequest.ts +++ b/packages/trueforge-sdk/src/api/resources/agents/client/requests/CreateAgentRequest.ts @@ -14,6 +14,8 @@ import type * as TrueForge from "../../../../index.js"; * } */ export interface CreateAgentRequest { + /** Short summary of what the agent does. */ + description?: string; manifest: TrueForge.AgentSpec; name: TrueForge.ResourceName; } diff --git a/packages/trueforge-sdk/src/api/resources/agents/client/requests/UpdateAgentRequest.ts b/packages/trueforge-sdk/src/api/resources/agents/client/requests/UpdateAgentRequest.ts index b92a833d5..ac9b6d718 100644 --- a/packages/trueforge-sdk/src/api/resources/agents/client/requests/UpdateAgentRequest.ts +++ b/packages/trueforge-sdk/src/api/resources/agents/client/requests/UpdateAgentRequest.ts @@ -13,5 +13,7 @@ import type * as TrueForge from "../../../../index.js"; * } */ export interface UpdateAgentRequest { + /** Short summary of what the agent does. */ + description?: string; manifest: TrueForge.AgentSpec; } diff --git a/packages/trueforge-sdk/src/api/types/Agent.ts b/packages/trueforge-sdk/src/api/types/Agent.ts index a912c1c39..7c6783773 100644 --- a/packages/trueforge-sdk/src/api/types/Agent.ts +++ b/packages/trueforge-sdk/src/api/types/Agent.ts @@ -4,6 +4,8 @@ import type * as TrueForge from "../index.js"; export interface Agent { createdBySubject: TrueForge.CreatedBySubject; + /** Short summary of what the agent does. */ + description: string; /** Immutable server-generated agent identifier. */ id: string; manifest: TrueForge.AgentSpec; diff --git a/packages/trueforge-sdk/src/serialization/resources/agents/client/requests/CreateAgentRequest.ts b/packages/trueforge-sdk/src/serialization/resources/agents/client/requests/CreateAgentRequest.ts index 27456816a..93993fa7c 100644 --- a/packages/trueforge-sdk/src/serialization/resources/agents/client/requests/CreateAgentRequest.ts +++ b/packages/trueforge-sdk/src/serialization/resources/agents/client/requests/CreateAgentRequest.ts @@ -10,12 +10,14 @@ export const CreateAgentRequest: core.serialization.Schema< serializers.CreateAgentRequest.Raw, TrueForge.CreateAgentRequest > = core.serialization.object({ + description: core.serialization.string().optional(), manifest: AgentSpec, name: ResourceName, }); export declare namespace CreateAgentRequest { export interface Raw { + description?: string | null; manifest: AgentSpec.Raw; name: ResourceName.Raw; } diff --git a/packages/trueforge-sdk/src/serialization/resources/agents/client/requests/UpdateAgentRequest.ts b/packages/trueforge-sdk/src/serialization/resources/agents/client/requests/UpdateAgentRequest.ts index 5ead63d5b..e9431c780 100644 --- a/packages/trueforge-sdk/src/serialization/resources/agents/client/requests/UpdateAgentRequest.ts +++ b/packages/trueforge-sdk/src/serialization/resources/agents/client/requests/UpdateAgentRequest.ts @@ -9,11 +9,13 @@ export const UpdateAgentRequest: core.serialization.Schema< serializers.UpdateAgentRequest.Raw, TrueForge.UpdateAgentRequest > = core.serialization.object({ + description: core.serialization.string().optional(), manifest: AgentSpec, }); export declare namespace UpdateAgentRequest { export interface Raw { + description?: string | null; manifest: AgentSpec.Raw; } } diff --git a/packages/trueforge-sdk/src/serialization/types/Agent.ts b/packages/trueforge-sdk/src/serialization/types/Agent.ts index 21ce3103a..031634495 100644 --- a/packages/trueforge-sdk/src/serialization/types/Agent.ts +++ b/packages/trueforge-sdk/src/serialization/types/Agent.ts @@ -10,6 +10,7 @@ import { ResourceName } from "./ResourceName.js"; export const Agent: core.serialization.ObjectSchema = core.serialization.object( { createdBySubject: core.serialization.property("created_by_subject", CreatedBySubject), + description: core.serialization.string(), id: core.serialization.string(), manifest: AgentSpec, name: ResourceName, @@ -19,6 +20,7 @@ export const Agent: core.serialization.ObjectSchema { subject_id: "subject_id", subject_type: "subject_type", }, + description: "description", id: "id", manifest: { model: { name: "name" } }, name: "name", @@ -35,6 +36,7 @@ describe("AgentsClient", () => { subjectId: "subject_id", subjectType: "subject_type", }, + description: "description", id: "id", manifest: { model: { @@ -71,6 +73,7 @@ describe("AgentsClient", () => { subject_id: "subject_id", subject_type: "subject_type", }, + description: "description", id: "id", manifest: { instructions: "instructions", @@ -108,6 +111,7 @@ describe("AgentsClient", () => { subjectId: "subject_id", subjectType: "subject_type", }, + description: "description", id: "id", manifest: { instructions: "instructions", @@ -231,6 +235,7 @@ describe("AgentsClient", () => { subject_id: "subject_id", subject_type: "subject_type", }, + description: "description", id: "id", manifest: { instructions: "instructions", @@ -260,6 +265,7 @@ describe("AgentsClient", () => { subjectId: "subject_id", subjectType: "subject_type", }, + description: "description", id: "id", manifest: { instructions: "instructions", @@ -321,6 +327,7 @@ describe("AgentsClient", () => { subject_id: "subject_id", subject_type: "subject_type", }, + description: "description", id: "id", manifest: { instructions: "instructions", @@ -357,6 +364,7 @@ describe("AgentsClient", () => { subjectId: "subject_id", subjectType: "subject_type", }, + description: "description", id: "id", manifest: { instructions: "instructions", From 98fbb22bec166a9a7a2130db8f67292dd210e50f Mon Sep 17 00:00:00 2001 From: Bhavesh Patel Date: Fri, 11 Sep 2026 12:34:31 +0530 Subject: [PATCH 4/8] migrations updated --- ...agent_description.ts => 20260911_000001_agent_description.ts} | 0 packages/trueforge/tests/unit/apis/schedules.test.ts | 1 + 2 files changed, 1 insertion(+) rename packages/trueforge/src/db/sqlite/migrations/{20260909_000001_agent_description.ts => 20260911_000001_agent_description.ts} (100%) diff --git a/packages/trueforge/src/db/sqlite/migrations/20260909_000001_agent_description.ts b/packages/trueforge/src/db/sqlite/migrations/20260911_000001_agent_description.ts similarity index 100% rename from packages/trueforge/src/db/sqlite/migrations/20260909_000001_agent_description.ts rename to packages/trueforge/src/db/sqlite/migrations/20260911_000001_agent_description.ts diff --git a/packages/trueforge/tests/unit/apis/schedules.test.ts b/packages/trueforge/tests/unit/apis/schedules.test.ts index c572b31f2..6d710f975 100644 --- a/packages/trueforge/tests/unit/apis/schedules.test.ts +++ b/packages/trueforge/tests/unit/apis/schedules.test.ts @@ -438,6 +438,7 @@ describe('internal schedule execution', () => { subject_display_name: 'alice', }, name: 'reporter', + description: 'reporter description', manifest: AgentSpecSchema.parse({ model: { name: 'test-provider/test-model' }, instructions: 'test' }), external_id: 'reporter-external-id', }); From e93553fa6634ba23eaa15dc212c8b3ad9cf5d941 Mon Sep 17 00:00:00 2001 From: Bhavesh Patel Date: Fri, 11 Sep 2026 13:14:56 +0530 Subject: [PATCH 5/8] Make description required --- packages/trueforge/src/schemas/agent.ts | 3 +- .../tests/db/agentStoreContractSuite.ts | 36 +++++++++---------- .../tests/db/scheduleDispatchContractSuite.ts | 2 +- .../tests/db/scheduleStoreContractSuite.ts | 2 +- .../trueforge/tests/unit/apis/agents.test.ts | 11 ++++++ .../tests/unit/apis/schedules.test.ts | 4 +-- .../tests/unit/apis/sessionHttp.test.ts | 10 +++--- .../trueforge/tests/unit/apis/turns.test.ts | 4 +-- .../unit/truefoundry/accessToken.test.ts | 2 +- 9 files changed, 43 insertions(+), 31 deletions(-) diff --git a/packages/trueforge/src/schemas/agent.ts b/packages/trueforge/src/schemas/agent.ts index bebcc7346..8fde081b7 100644 --- a/packages/trueforge/src/schemas/agent.ts +++ b/packages/trueforge/src/schemas/agent.ts @@ -13,6 +13,7 @@ export const AGENT_DESCRIPTION_MAX_LENGTH = 1024; export const AgentDescriptionSchema = z .string() .trim() + .min(1) .max(AGENT_DESCRIPTION_MAX_LENGTH) .describe('Short summary of what the agent does.'); @@ -22,7 +23,7 @@ export const CreateAgentRequestSchema = z name: NameSchema.refine(name => !RESERVED_AGENT_NAMES.has(name), { message: 'Agent name is reserved, cannot be used', }), - description: AgentDescriptionSchema.default(''), + description: AgentDescriptionSchema, manifest: AgentSpecSchema, }) .strict() diff --git a/packages/trueforge/tests/db/agentStoreContractSuite.ts b/packages/trueforge/tests/db/agentStoreContractSuite.ts index 246e12c07..e07015c99 100644 --- a/packages/trueforge/tests/db/agentStoreContractSuite.ts +++ b/packages/trueforge/tests/db/agentStoreContractSuite.ts @@ -112,7 +112,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'research', - description: '', + description: 'Test agent.', manifest: manifest(), external_id: null, }); @@ -122,7 +122,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'research', - description: '', + description: 'Test agent.', manifest: manifest(), external_id: null, }), @@ -135,7 +135,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'zeta', - description: '', + description: 'Test agent.', manifest: manifest(), external_id: null, }); @@ -143,7 +143,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'alpha', - description: '', + description: 'Test agent.', manifest: manifest({ instructions: 'Alpha agent.' }), external_id: null, }); @@ -151,7 +151,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: 'other-tenant', created_by_subject: CREATED_BY_SUBJECT, name: 'research', - description: '', + description: 'Test agent.', manifest: manifest(), external_id: null, }); @@ -167,7 +167,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'local-only', - description: '', + description: 'Test agent.', manifest: manifest(), external_id: null, }); @@ -175,7 +175,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'linked', - description: '', + description: 'Test agent.', manifest: manifest(), external_id: 'sf-agent-1', }); @@ -183,7 +183,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'other-linked', - description: '', + description: 'Test agent.', manifest: manifest(), external_id: 'sf-agent-2', }); @@ -204,7 +204,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'research', - description: '', + description: 'Test agent.', manifest: manifest(), external_id: null, }); @@ -218,7 +218,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'research', - description: '', + description: 'Test agent.', manifest: manifest(), external_id: 'sf-agent-1', }); @@ -232,7 +232,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'alpha', - description: '', + description: 'Test agent.', manifest: manifest(), external_id: 'shared-key', }); @@ -241,7 +241,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'beta', - description: '', + description: 'Test agent.', manifest: manifest(), external_id: 'shared-key', }), @@ -250,7 +250,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: 'other-tenant', created_by_subject: CREATED_BY_SUBJECT, name: 'alpha', - description: '', + description: 'Test agent.', manifest: manifest(), external_id: 'shared-key', }); @@ -258,7 +258,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'gamma', - description: '', + description: 'Test agent.', manifest: manifest(), external_id: null, }); @@ -266,7 +266,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'delta', - description: '', + description: 'Test agent.', manifest: manifest(), external_id: null, }); @@ -278,7 +278,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'research', - description: '', + description: 'Test agent.', manifest: manifest(), external_id: null, }); @@ -302,7 +302,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'alpha', - description: '', + description: 'Test agent.', manifest: manifest(), external_id: 'shared-key', }); @@ -310,7 +310,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'beta', - description: '', + description: 'Test agent.', manifest: manifest(), external_id: null, }); diff --git a/packages/trueforge/tests/db/scheduleDispatchContractSuite.ts b/packages/trueforge/tests/db/scheduleDispatchContractSuite.ts index 7f704fb6d..e7b922036 100644 --- a/packages/trueforge/tests/db/scheduleDispatchContractSuite.ts +++ b/packages/trueforge/tests/db/scheduleDispatchContractSuite.ts @@ -71,7 +71,7 @@ export function runScheduleDispatchContractSuite(deps: { tenant_id: TENANT, created_by_subject: USER_SUBJECT, name: `agent-${String(Date.now())}-${String(seq)}`, - description: '', + description: 'Test agent.', manifest: AgentSpecSchema.parse({ model: { name: 'anthropic/claude-sonnet-4-6' }, instructions: 'Be helpful.', diff --git a/packages/trueforge/tests/db/scheduleStoreContractSuite.ts b/packages/trueforge/tests/db/scheduleStoreContractSuite.ts index 706bd6013..3f2470daa 100644 --- a/packages/trueforge/tests/db/scheduleStoreContractSuite.ts +++ b/packages/trueforge/tests/db/scheduleStoreContractSuite.ts @@ -31,7 +31,7 @@ export function runScheduleStoreContractSuite(deps: { tenant_id: TENANT, created_by_subject: USER_SUBJECT, name: `agent-${String(Date.now())}-${String(Math.random()).slice(2, 8)}`, - description: '', + description: 'Test agent.', manifest: AgentSpecSchema.parse({ model: { name: 'anthropic/claude-sonnet-4-6' }, instructions: 'Be helpful.', diff --git a/packages/trueforge/tests/unit/apis/agents.test.ts b/packages/trueforge/tests/unit/apis/agents.test.ts index 59de294c3..10aa12a61 100644 --- a/packages/trueforge/tests/unit/apis/agents.test.ts +++ b/packages/trueforge/tests/unit/apis/agents.test.ts @@ -38,6 +38,7 @@ const manifest = { const writeBody = { name: 'research', + description: 'Research helper.', manifest, }; @@ -241,11 +242,21 @@ describe('agents router', () => { '/', jsonInit('POST', { name: 'other', + description: 'Other agent.', manifest: { ...manifest, model: { name: 'missing/model' } }, }), ); expect(unknownModel.status).toBe(422); + const blankDescription = await router.request( + '/', + jsonInit('POST', { ...writeBody, name: 'blank-desc', description: ' ' }), + ); + expect(blankDescription.status).toBe(400); + + const missingDescription = await router.request('/', jsonInit('POST', { name: 'no-desc', manifest })); + expect(missingDescription.status).toBe(400); + const first = await router.request('/', jsonInit('POST', { ...writeBody, name: 'alpha' })); expect(first.status).toBe(201); diff --git a/packages/trueforge/tests/unit/apis/schedules.test.ts b/packages/trueforge/tests/unit/apis/schedules.test.ts index 6d710f975..7be02cf23 100644 --- a/packages/trueforge/tests/unit/apis/schedules.test.ts +++ b/packages/trueforge/tests/unit/apis/schedules.test.ts @@ -88,7 +88,7 @@ async function setup(authorizer: Authorizer = new TrueForgeAuthorizer()) { subject_display_name: 'alice', }, name: 'reporter', - description: '', + description: 'Test agent.', manifest: AgentSpecSchema.parse({ model: { name: 'test-provider/test-model' }, instructions: 'test' }), external_id: 'reporter-external-id', }); @@ -248,7 +248,7 @@ describe('schedule list agent_names filter', () => { subject_display_name: 'alice', }, name: 'reporter-two', - description: '', + description: 'Test agent.', manifest: AgentSpecSchema.parse({ model: { name: 'test-provider/test-model' }, instructions: 'test' }), external_id: null, }); diff --git a/packages/trueforge/tests/unit/apis/sessionHttp.test.ts b/packages/trueforge/tests/unit/apis/sessionHttp.test.ts index 853a52732..2112b6fd1 100644 --- a/packages/trueforge/tests/unit/apis/sessionHttp.test.ts +++ b/packages/trueforge/tests/unit/apis/sessionHttp.test.ts @@ -153,7 +153,7 @@ describe('sessions HTTP agent binding', () => { subject_display_name: STANDALONE_REQUEST_CONTEXT.subject.display_name, }, name: 'named-agent', - description: '', + description: 'Test agent.', manifest: AgentSpecSchema.parse({ model: { name: 'anthropic/claude-sonnet-4-6' }, instructions: 'from-registry', @@ -186,7 +186,7 @@ describe('sessions HTTP agent binding', () => { subject_display_name: STANDALONE_REQUEST_CONTEXT.subject.display_name, }, name: 'metrics-agent', - description: '', + description: 'Test agent.', manifest: inlineSpec, external_id: null, }); @@ -246,7 +246,7 @@ describe('sessions HTTP agent binding', () => { tenant_id: 'default', created_by_subject: { subject_id: 'owner', subject_type: 'user', subject_display_name: 'Owner' }, name: 'managed-agent', - description: '', + description: 'Test agent.', manifest: inlineSpec, external_id: 'managed-agent-external', }); @@ -427,7 +427,7 @@ describe('sessions HTTP agent binding', () => { subject_display_name: STANDALONE_REQUEST_CONTEXT.subject.display_name, }, name: 'named-agent', - description: '', + description: 'Test agent.', manifest: AgentSpecSchema.parse({ model: { name: 'anthropic/claude-sonnet-4-6' }, instructions: 'from-registry', @@ -609,7 +609,7 @@ describe('sessions HTTP agent binding', () => { subject_display_name: STANDALONE_REQUEST_CONTEXT.subject.display_name, }, name: 'forbidden-agent', - description: '', + description: 'Test agent.', manifest: inlineSpec, external_id: null, }); diff --git a/packages/trueforge/tests/unit/apis/turns.test.ts b/packages/trueforge/tests/unit/apis/turns.test.ts index 898761537..42c872ad9 100644 --- a/packages/trueforge/tests/unit/apis/turns.test.ts +++ b/packages/trueforge/tests/unit/apis/turns.test.ts @@ -124,7 +124,7 @@ describe('turns', () => { const agent = await agentStore.createAgent({ tenant_id: 'default', name: 'managed-agent', - description: '', + description: 'Test agent.', manifest: AgentSpecSchema.parse({ model: { name: 'test-provider/test-model' } }), external_id: 'managed-agent-external', created_by_subject: { subject_id: 'owner', subject_type: 'user', subject_display_name: 'Owner' }, @@ -434,7 +434,7 @@ describe('turns', () => { subject_display_name: STANDALONE_REQUEST_CONTEXT.subject.display_name, }, name: 'named-for-turn', - description: '', + description: 'Test agent.', manifest: AgentSpecSchema.parse({ model: { name: 'test-provider/test-model' }, instructions: 'test', diff --git a/packages/trueforge/tests/unit/truefoundry/accessToken.test.ts b/packages/trueforge/tests/unit/truefoundry/accessToken.test.ts index b3c9eb58f..932c61227 100644 --- a/packages/trueforge/tests/unit/truefoundry/accessToken.test.ts +++ b/packages/trueforge/tests/unit/truefoundry/accessToken.test.ts @@ -22,7 +22,7 @@ const AGENT: AgentRecord = { id: 'agent-1', tenant_id: 'acme', name: 'named', - description: '', + description: 'Test agent.', manifest: AgentSpecSchema.parse({ model: { name: 'p/m' } }), external_id: 'ext-agent', created_by_subject: { subject_id: 'user-1', subject_type: 'user', subject_display_name: 'User' }, From 77664b52564447d09441c44cdb094343e3d347df Mon Sep 17 00:00:00 2001 From: Bhavesh Patel Date: Fri, 11 Sep 2026 17:00:53 +0530 Subject: [PATCH 6/8] comment addressed --- packages/trueforge/src/schemas/agent.ts | 7 +------ .../unit/truefoundry/TrueFoundryAgentStore.test.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/packages/trueforge/src/schemas/agent.ts b/packages/trueforge/src/schemas/agent.ts index 36ceff751..4a77b7f4f 100644 --- a/packages/trueforge/src/schemas/agent.ts +++ b/packages/trueforge/src/schemas/agent.ts @@ -14,12 +14,7 @@ const RESERVED_AGENT_NAMES = new Set(['tfg', 'trueforge']); export const AGENT_DESCRIPTION_MAX_LENGTH = 1024; -export const AgentDescriptionSchema = z - .string() - .trim() - .min(1) - .max(AGENT_DESCRIPTION_MAX_LENGTH) - .describe('Short summary of what the agent does.'); +export const AgentDescriptionSchema = z.string().trim().min(1).max(AGENT_DESCRIPTION_MAX_LENGTH); /** Create body: unique immutable `name` plus manifest. `id` is never client-supplied. */ export const CreateAgentRequestSchema = z diff --git a/packages/trueforge/tests/unit/truefoundry/TrueFoundryAgentStore.test.ts b/packages/trueforge/tests/unit/truefoundry/TrueFoundryAgentStore.test.ts index 0e06ff43d..9d66285fe 100644 --- a/packages/trueforge/tests/unit/truefoundry/TrueFoundryAgentStore.test.ts +++ b/packages/trueforge/tests/unit/truefoundry/TrueFoundryAgentStore.test.ts @@ -59,7 +59,7 @@ function record(overrides: Partial = {}): AgentRecord { id: 'agent-1', tenant_id: TENANT, name: 'research', - description: '', + description: 'research', manifest: manifest(), external_id: null, created_by_subject: CREATED_BY_SUBJECT, @@ -184,7 +184,7 @@ describe('TrueFoundryAgentStore', () => { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'research', - description: '', + description: 'research', manifest: manifest({ mcp_servers: [{ name: 'slack' }] }), external_id: null, }, @@ -222,7 +222,7 @@ describe('TrueFoundryAgentStore', () => { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'research', - description: '', + description: 'research', manifest: manifest(), external_id: null, }, @@ -307,7 +307,7 @@ describe('TrueFoundryAgentStore', () => { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'research', - description: '', + description: 'research', manifest: manifest(), external_id: null, }, @@ -338,7 +338,7 @@ describe('TrueFoundryAgentStore', () => { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'research', - description: '', + description: 'research', manifest: manifest(), external_id: null, }, @@ -375,7 +375,7 @@ describe('TrueFoundryAgentStore', () => { tenant_id: TENANT, created_by_subject: CREATED_BY_SUBJECT, name: 'research', - description: '', + description: 'research', manifest: manifest(), external_id: null, }, From ca28e5f502bc4af1fdfcc01d960703b6a4aff460 Mon Sep 17 00:00:00 2001 From: "trueforge-dev-bot[bot]" Date: Fri, 11 Sep 2026 11:33:10 +0000 Subject: [PATCH 7/8] Regenerate OpenAPI document and TypeScript SDK --- .github/fern/openapi/openapi.json | 8 ++++---- docs/openapi.json | 8 ++++---- packages/trueforge-sdk/reference.md | 1 + .../src/api/resources/agents/client/Client.ts | 1 + .../agents/client/requests/CreateAgentRequest.ts | 4 ++-- .../agents/client/requests/UpdateAgentRequest.ts | 1 - packages/trueforge-sdk/src/api/types/Agent.ts | 1 - .../agents/client/requests/CreateAgentRequest.ts | 4 ++-- packages/trueforge-sdk/tests/wire/agents.test.ts | 12 ++++++++---- 9 files changed, 22 insertions(+), 18 deletions(-) diff --git a/.github/fern/openapi/openapi.json b/.github/fern/openapi/openapi.json index 69e5d4124..357154849 100644 --- a/.github/fern/openapi/openapi.json +++ b/.github/fern/openapi/openapi.json @@ -30,8 +30,8 @@ "$ref": "#/components/schemas/CreatedBySubject" }, "description": { - "description": "Short summary of what the agent does.", "maxLength": 1024, + "minLength": 1, "type": "string" }, "id": { @@ -989,9 +989,8 @@ "additionalProperties": false, "properties": { "description": { - "default": "", - "description": "Short summary of what the agent does.", "maxLength": 1024, + "minLength": 1, "type": "string" }, "manifest": { @@ -1003,6 +1002,7 @@ }, "required": [ "name", + "description", "manifest" ], "type": "object" @@ -5343,8 +5343,8 @@ "additionalProperties": false, "properties": { "description": { - "description": "Short summary of what the agent does.", "maxLength": 1024, + "minLength": 1, "type": "string" }, "manifest": { diff --git a/docs/openapi.json b/docs/openapi.json index 69e5d4124..357154849 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -30,8 +30,8 @@ "$ref": "#/components/schemas/CreatedBySubject" }, "description": { - "description": "Short summary of what the agent does.", "maxLength": 1024, + "minLength": 1, "type": "string" }, "id": { @@ -989,9 +989,8 @@ "additionalProperties": false, "properties": { "description": { - "default": "", - "description": "Short summary of what the agent does.", "maxLength": 1024, + "minLength": 1, "type": "string" }, "manifest": { @@ -1003,6 +1002,7 @@ }, "required": [ "name", + "description", "manifest" ], "type": "object" @@ -5343,8 +5343,8 @@ "additionalProperties": false, "properties": { "description": { - "description": "Short summary of what the agent does.", "maxLength": 1024, + "minLength": 1, "type": "string" }, "manifest": { diff --git a/packages/trueforge-sdk/reference.md b/packages/trueforge-sdk/reference.md index 07b83e433..98c1d13f3 100644 --- a/packages/trueforge-sdk/reference.md +++ b/packages/trueforge-sdk/reference.md @@ -158,6 +158,7 @@ Creates an agent and allocates an immutable id. Fails if `name` is already taken ```typescript await client.agents.create({ + description: "description", manifest: { model: { name: "name" diff --git a/packages/trueforge-sdk/src/api/resources/agents/client/Client.ts b/packages/trueforge-sdk/src/api/resources/agents/client/Client.ts index 2ba821682..5a3ff0a29 100644 --- a/packages/trueforge-sdk/src/api/resources/agents/client/Client.ts +++ b/packages/trueforge-sdk/src/api/resources/agents/client/Client.ts @@ -151,6 +151,7 @@ export class AgentsClient { * * @example * await client.agents.create({ + * description: "description", * manifest: { * model: { * name: "name" diff --git a/packages/trueforge-sdk/src/api/resources/agents/client/requests/CreateAgentRequest.ts b/packages/trueforge-sdk/src/api/resources/agents/client/requests/CreateAgentRequest.ts index 42f88b580..bec02b543 100644 --- a/packages/trueforge-sdk/src/api/resources/agents/client/requests/CreateAgentRequest.ts +++ b/packages/trueforge-sdk/src/api/resources/agents/client/requests/CreateAgentRequest.ts @@ -5,6 +5,7 @@ import type * as TrueForge from "../../../../index.js"; /** * @example * { + * description: "description", * manifest: { * model: { * name: "name" @@ -14,8 +15,7 @@ import type * as TrueForge from "../../../../index.js"; * } */ export interface CreateAgentRequest { - /** Short summary of what the agent does. */ - description?: string; + description: string; manifest: TrueForge.AgentSpec; name: TrueForge.ResourceName; } diff --git a/packages/trueforge-sdk/src/api/resources/agents/client/requests/UpdateAgentRequest.ts b/packages/trueforge-sdk/src/api/resources/agents/client/requests/UpdateAgentRequest.ts index ac9b6d718..4b4d7470b 100644 --- a/packages/trueforge-sdk/src/api/resources/agents/client/requests/UpdateAgentRequest.ts +++ b/packages/trueforge-sdk/src/api/resources/agents/client/requests/UpdateAgentRequest.ts @@ -13,7 +13,6 @@ import type * as TrueForge from "../../../../index.js"; * } */ export interface UpdateAgentRequest { - /** Short summary of what the agent does. */ description?: string; manifest: TrueForge.AgentSpec; } diff --git a/packages/trueforge-sdk/src/api/types/Agent.ts b/packages/trueforge-sdk/src/api/types/Agent.ts index 7c6783773..497c56a02 100644 --- a/packages/trueforge-sdk/src/api/types/Agent.ts +++ b/packages/trueforge-sdk/src/api/types/Agent.ts @@ -4,7 +4,6 @@ import type * as TrueForge from "../index.js"; export interface Agent { createdBySubject: TrueForge.CreatedBySubject; - /** Short summary of what the agent does. */ description: string; /** Immutable server-generated agent identifier. */ id: string; diff --git a/packages/trueforge-sdk/src/serialization/resources/agents/client/requests/CreateAgentRequest.ts b/packages/trueforge-sdk/src/serialization/resources/agents/client/requests/CreateAgentRequest.ts index 93993fa7c..1fff2647c 100644 --- a/packages/trueforge-sdk/src/serialization/resources/agents/client/requests/CreateAgentRequest.ts +++ b/packages/trueforge-sdk/src/serialization/resources/agents/client/requests/CreateAgentRequest.ts @@ -10,14 +10,14 @@ export const CreateAgentRequest: core.serialization.Schema< serializers.CreateAgentRequest.Raw, TrueForge.CreateAgentRequest > = core.serialization.object({ - description: core.serialization.string().optional(), + description: core.serialization.string(), manifest: AgentSpec, name: ResourceName, }); export declare namespace CreateAgentRequest { export interface Raw { - description?: string | null; + description: string; manifest: AgentSpec.Raw; name: ResourceName.Raw; } diff --git a/packages/trueforge-sdk/tests/wire/agents.test.ts b/packages/trueforge-sdk/tests/wire/agents.test.ts index 73a45f443..111ecdc09 100644 --- a/packages/trueforge-sdk/tests/wire/agents.test.ts +++ b/packages/trueforge-sdk/tests/wire/agents.test.ts @@ -95,7 +95,7 @@ describe("AgentsClient", () => { test("create (1)", async () => { const server = mockServerPool.createServer(); const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); - const rawRequestBody = { manifest: { model: { name: "name" } }, name: "name" }; + const rawRequestBody = { description: "description", manifest: { model: { name: "name" } }, name: "name" }; const rawResponseBody = { data: { created_by_subject: { @@ -127,6 +127,7 @@ describe("AgentsClient", () => { .build(); const response = await client.agents.create({ + description: "description", manifest: { model: { name: "name", @@ -176,7 +177,7 @@ describe("AgentsClient", () => { test("create (2)", async () => { const server = mockServerPool.createServer(); const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); - const rawRequestBody = { manifest: { model: { name: "x" } }, name: "xy" }; + const rawRequestBody = { description: "x", manifest: { model: { name: "x" } }, name: "xy" }; const rawResponseBody = { error: { message: "message" } }; server @@ -190,6 +191,7 @@ describe("AgentsClient", () => { await expect(async () => { return await client.agents.create({ + description: "x", manifest: { model: { name: "x", @@ -203,7 +205,7 @@ describe("AgentsClient", () => { test("create (3)", async () => { const server = mockServerPool.createServer(); const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); - const rawRequestBody = { manifest: { model: { name: "x" } }, name: "xy" }; + const rawRequestBody = { description: "x", manifest: { model: { name: "x" } }, name: "xy" }; const rawResponseBody = { error: { message: "message" } }; server @@ -217,6 +219,7 @@ describe("AgentsClient", () => { await expect(async () => { return await client.agents.create({ + description: "x", manifest: { model: { name: "x", @@ -230,7 +233,7 @@ describe("AgentsClient", () => { test("create (4)", async () => { const server = mockServerPool.createServer(); const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); - const rawRequestBody = { manifest: { model: { name: "x" } }, name: "xy" }; + const rawRequestBody = { description: "x", manifest: { model: { name: "x" } }, name: "xy" }; const rawResponseBody = { error: { message: "message" } }; server @@ -244,6 +247,7 @@ describe("AgentsClient", () => { await expect(async () => { return await client.agents.create({ + description: "x", manifest: { model: { name: "x", From dd80759a31eac2c4a2f525dd0351efe9f294a831 Mon Sep 17 00:00:00 2001 From: harshil-2096 <100749155+harshil-2096@users.noreply.github.com> Date: Fri, 11 Sep 2026 18:21:26 +0530 Subject: [PATCH 8/8] Chore: Add agent description support in UI (#707) * Regenerate OpenAPI document and TypeScript SDK * Chore: Add agent description support in UI * Enhancement: Implement support for agent descriptions across UI components * chore: update @truefoundry/assistant-ui-runtime to version 0.1.37 in package.json files * Refactor: Simplify agent description handling and improve UI consistency * Fix: Ensure agent description is displayed in the save dialog * Refactor: Replace description text with tooltip in AgentLibraryRow and remove unused description in AgentOverview * Enhancement: Add description display in AgentOverview component * Add description field to agent records in tests and update AgentsLibrary for better handling of agent descriptions - Added 'description' field to agent records in agentStoreContractSuite, TrueFoundryMcpServerStore.test, and TrueFoundryModelProviderStore.test. - Updated AgentsLibrary component to improve description handling, ensuring proper truncation and avoiding name-echo descriptions. - Enhanced test cases to verify description truncation and visibility in the AgentsLibrary. --------- Co-authored-by: trueforge-dev-bot[bot] Co-authored-by: Govinda Vashishtha <57435703+govindavashishtha@users.noreply.github.com> Co-authored-by: Cursor Co-authored-by: Bhavesh Patel --- .changeset/agent-description-ui.md | 5 + packages/frontend/package.json | 2 +- packages/trueforge-ui/package.json | 2 +- .../src/atoms/AgentOverflowMenu.tsx | 3 + .../trueforge-ui/src/atoms/AgentsLibrary.tsx | 19 +- .../src/atoms/SaveAgentButton.tsx | 11 +- .../trueforge-ui/src/atoms/SaveAgentForm.tsx | 33 +- .../agent-details/AgentDetailsHeader.tsx | 2 + .../src/atoms/agent-details/AgentOverview.tsx | 286 ++++++++--------- .../atoms/agent-details/AgentOverviewCard.tsx | 2 +- .../src/atoms/agent-details/types.ts | 2 +- .../agentSessionsServer.ts | 1 + .../builderServer.ts | 19 +- .../src/server/ShellModeContext.tsx | 9 +- .../test/atoms/AgentDetailsPage.test.tsx | 2 + .../test/atoms/AgentsLibrary.test.tsx | 40 ++- .../test/atoms/SaveAgentButton.test.tsx | 70 ++++- .../test/containers/TrueForgeUI.test.tsx | 2 +- .../agentSessionsServer.test.ts | 7 +- .../harnessBuilderServer.test.ts | 59 +++- .../tests/db/agentStoreContractSuite.ts | 6 + .../TrueFoundryMcpServerStore.test.ts | 1 + .../TrueFoundryModelProviderStore.test.ts | 1 + pnpm-lock.yaml | 291 +----------------- 24 files changed, 420 insertions(+), 455 deletions(-) create mode 100644 .changeset/agent-description-ui.md diff --git a/.changeset/agent-description-ui.md b/.changeset/agent-description-ui.md new file mode 100644 index 000000000..41547498f --- /dev/null +++ b/.changeset/agent-description-ui.md @@ -0,0 +1,5 @@ +--- +"@truefoundry/trueforge-ui": patch +--- + +Round-trip agent description through save/load and show it in the library and agent details. diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 7df484c6f..693fa3a78 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -26,7 +26,7 @@ "@assistant-ui/core": "0.2.22", "@assistant-ui/react": "0.14.27", "@assistant-ui/store": "0.2.21", - "@truefoundry/assistant-ui-runtime": "0.1.36", + "@truefoundry/assistant-ui-runtime": "0.1.37", "@truefoundry/trueforge-sdk": "workspace:*", "@truefoundry/trueforge-ui": "workspace:*", "monaco-editor": "^0.52.0", diff --git a/packages/trueforge-ui/package.json b/packages/trueforge-ui/package.json index c2000135a..1607b781f 100644 --- a/packages/trueforge-ui/package.json +++ b/packages/trueforge-ui/package.json @@ -95,7 +95,7 @@ "@openuidev/react-headless": "^0.9.4", "@openuidev/react-lang": "^0.2.9", "@openuidev/react-ui": "^0.13.2", - "@truefoundry/assistant-ui-runtime": "0.1.36", + "@truefoundry/assistant-ui-runtime": "0.1.37", "@truefoundry/trueforge-sdk": "workspace:*", "chart.js": "^4.5.1", "clsx": "^2.1.1", diff --git a/packages/trueforge-ui/src/atoms/AgentOverflowMenu.tsx b/packages/trueforge-ui/src/atoms/AgentOverflowMenu.tsx index c58d8352e..66ab7be54 100644 --- a/packages/trueforge-ui/src/atoms/AgentOverflowMenu.tsx +++ b/packages/trueforge-ui/src/atoms/AgentOverflowMenu.tsx @@ -20,6 +20,7 @@ export function cloneAgentName(agentName: string): string { export type AgentOverflowMenuProps = { agentName: string; + description?: string; agentSpec?: AgentSpec; /** Edit / Clone / Delete when composer is enabled. */ canMutate: boolean; @@ -37,6 +38,7 @@ type PendingAction = 'clone' | 'delete' | null; export function AgentOverflowMenu({ agentName, + description, agentSpec, canMutate, canUse = true, @@ -72,6 +74,7 @@ export function AgentOverflowMenu({ try { await builder.saveAgent({ agentName: clonedName, + ...(description === undefined ? {} : { description }), agentSpec, intent: 'create', }); diff --git a/packages/trueforge-ui/src/atoms/AgentsLibrary.tsx b/packages/trueforge-ui/src/atoms/AgentsLibrary.tsx index df8b4ce1c..436e07639 100644 --- a/packages/trueforge-ui/src/atoms/AgentsLibrary.tsx +++ b/packages/trueforge-ui/src/atoms/AgentsLibrary.tsx @@ -189,10 +189,14 @@ export function AgentLibraryRow({ const skillsTitle = skillNames.length ? skillNames.join(', ') : `${skillsCount} skills`; const hasConfiguration = modelLabel != null || skillsCount > 0 || mcpCount > 0; const hasNoSchedules = scheduleSummary != null && scheduleSummary.count === 0; + const storedDescription = agent.description?.trim() || null; + // Create falls back to name when description is missing; don't echo it under the title. + const description = storedDescription != null && storedDescription !== agent.name ? storedDescription : null; return ( - + {/* Fixed width so truncate works; 24rem = 1.5× the prior min-w-64 name column. */} + {onOpen == null ? ( {agent.name} ) : ( @@ -205,6 +209,15 @@ export function AgentLibraryRow({ {agent.name} )} + {description ? ( + + {description} + + ) : null} {hasConfiguration ? ( @@ -276,6 +289,7 @@ export function AgentLibraryRow({ - Agent name + Agent name Configuration {showCreatedByColumn ? Created by : null} {showSchedulesColumn ? Schedules : null} diff --git a/packages/trueforge-ui/src/atoms/SaveAgentButton.tsx b/packages/trueforge-ui/src/atoms/SaveAgentButton.tsx index e9c5c4fe8..c764dfb3e 100644 --- a/packages/trueforge-ui/src/atoms/SaveAgentButton.tsx +++ b/packages/trueforge-ui/src/atoms/SaveAgentButton.tsx @@ -86,6 +86,7 @@ function SaveAgentButtonContent({ const [open, setOpen] = useState(false); const [intent, setIntent] = useState('create'); const [name, setName] = useState(''); + const [description, setDescription] = useState(''); const [draftSpec, setDraftSpec] = useState(null); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); @@ -93,6 +94,7 @@ function SaveAgentButtonContent({ const close = () => { if (saving) return; setOpen(false); + setDescription(''); setDraftSpec(null); setError(null); }; @@ -111,6 +113,7 @@ function SaveAgentButtonContent({ const currentName = shell?.mode.status === 'active' ? (shell.mode.agentName ?? shell.mode.agentId ?? '') : ''; setIntent(currentName ? 'update' : 'create'); setName(currentName); + setDescription(currentName && shell?.mode.status === 'active' ? (shell.mode.description ?? '') : ''); setDraftSpec(cloneAgentSpec(latestAgentSpec)); setOpen(true); }; @@ -120,11 +123,14 @@ function SaveAgentButtonContent({ if (intent === 'update' && !canManageAgent) return; const normalizedName = name.trim(); if (!normalizedName || !draftSpec.model.name.trim()) return; + const normalizedDescription = description.trim(); + if (!normalizedDescription) return; setSaving(true); setError(null); try { const result = await builder.saveAgent({ agentName: normalizedName, + ...(normalizedDescription ? { description: normalizedDescription } : {}), agentSpec: draftSpec, intent, sessionId: draftSessionId, @@ -133,10 +139,12 @@ function SaveAgentButtonContent({ shell?.bindMutableAgent({ agentId: result.agentId ?? normalizedName, agentName: normalizedName, + ...(normalizedDescription ? { description: normalizedDescription } : {}), agentSpec: draftSpec, }); shell?.invalidateAgentsList(); setOpen(false); + setDescription(''); setDraftSpec(null); } catch (caught) { setError(getErrorMessage(caught, 'Could not save agent')); @@ -180,11 +188,12 @@ function SaveAgentButtonContent({ void save()} /> diff --git a/packages/trueforge-ui/src/atoms/SaveAgentForm.tsx b/packages/trueforge-ui/src/atoms/SaveAgentForm.tsx index 8c6d0f236..780aecebc 100644 --- a/packages/trueforge-ui/src/atoms/SaveAgentForm.tsx +++ b/packages/trueforge-ui/src/atoms/SaveAgentForm.tsx @@ -9,11 +9,12 @@ import { Button } from './primitives/Button.js'; export type SaveAgentFormProps = { intent: 'create' | 'update'; name: string; + description: string; spec: AgentSpec; saving: boolean; error: string | null; onNameChange: (name: string) => void; - onChange: (spec: AgentSpec) => void; + onDescriptionChange: (description: string) => void; onCancel: () => void; onSave: () => void; }; @@ -21,10 +22,12 @@ export type SaveAgentFormProps = { export function SaveAgentForm({ intent, name, + description, spec, saving, error, onNameChange, + onDescriptionChange, onCancel, onSave, }: SaveAgentFormProps) { @@ -36,6 +39,8 @@ export function SaveAgentForm({ errorRef.current?.scrollIntoView?.({ block: 'nearest', behavior: 'smooth' }); }, [error]); + const canSave = name.trim() !== '' && spec.model.name.trim() !== '' && description.trim() !== ''; + return (
@@ -50,18 +55,18 @@ export function SaveAgentForm({ /> - {/* TODO: Uncomment the description field when the backend supports description */} - {/*