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/.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/.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/.github/fern/openapi/openapi.json b/.github/fern/openapi/openapi.json index 9b076745f..357154849 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": { + "maxLength": 1024, + "minLength": 1, + "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,11 @@ "CreateAgentRequest": { "additionalProperties": false, "properties": { + "description": { + "maxLength": 1024, + "minLength": 1, + "type": "string" + }, "manifest": { "$ref": "#/components/schemas/AgentSpec" }, @@ -991,6 +1002,7 @@ }, "required": [ "name", + "description", "manifest" ], "type": "object" @@ -5330,6 +5342,11 @@ "UpdateAgentRequest": { "additionalProperties": false, "properties": { + "description": { + "maxLength": 1024, + "minLength": 1, + "type": "string" + }, "manifest": { "$ref": "#/components/schemas/AgentSpec" } @@ -6370,7 +6387,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 9b076745f..357154849 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -29,6 +29,11 @@ "created_by_subject": { "$ref": "#/components/schemas/CreatedBySubject" }, + "description": { + "maxLength": 1024, + "minLength": 1, + "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,11 @@ "CreateAgentRequest": { "additionalProperties": false, "properties": { + "description": { + "maxLength": 1024, + "minLength": 1, + "type": "string" + }, "manifest": { "$ref": "#/components/schemas/AgentSpec" }, @@ -991,6 +1002,7 @@ }, "required": [ "name", + "description", "manifest" ], "type": "object" @@ -5330,6 +5342,11 @@ "UpdateAgentRequest": { "additionalProperties": false, "properties": { + "description": { + "maxLength": 1024, + "minLength": 1, + "type": "string" + }, "manifest": { "$ref": "#/components/schemas/AgentSpec" } @@ -6370,7 +6387,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/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-sdk/reference.md b/packages/trueforge-sdk/reference.md index fe932a62d..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" @@ -275,7 +276,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 c2aece33d..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" @@ -346,7 +347,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..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,6 +15,7 @@ import type * as TrueForge from "../../../../index.js"; * } */ export interface CreateAgentRequest { + 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..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,5 +13,6 @@ import type * as TrueForge from "../../../../index.js"; * } */ export interface UpdateAgentRequest { + 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..497c56a02 100644 --- a/packages/trueforge-sdk/src/api/types/Agent.ts +++ b/packages/trueforge-sdk/src/api/types/Agent.ts @@ -4,6 +4,7 @@ import type * as TrueForge from "../index.js"; export interface Agent { createdBySubject: TrueForge.CreatedBySubject; + 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..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,12 +10,14 @@ export const CreateAgentRequest: core.serialization.Schema< serializers.CreateAgentRequest.Raw, TrueForge.CreateAgentRequest > = core.serialization.object({ + description: core.serialization.string(), manifest: AgentSpec, name: ResourceName, }); export declare namespace CreateAgentRequest { export interface Raw { + description: string; 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", @@ -41,6 +42,7 @@ describe("AgentsClient", () => { subjectId: "subject_id", subjectType: "subject_type", }, + description: "description", id: "id", manifest: { model: { @@ -93,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: { @@ -101,6 +103,7 @@ describe("AgentsClient", () => { subject_id: "subject_id", subject_type: "subject_type", }, + description: "description", id: "id", manifest: { instructions: "instructions", @@ -124,6 +127,7 @@ describe("AgentsClient", () => { .build(); const response = await client.agents.create({ + description: "description", manifest: { model: { name: "name", @@ -138,6 +142,7 @@ describe("AgentsClient", () => { subjectId: "subject_id", subjectType: "subject_type", }, + description: "description", id: "id", manifest: { instructions: "instructions", @@ -172,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 @@ -186,6 +191,7 @@ describe("AgentsClient", () => { await expect(async () => { return await client.agents.create({ + description: "x", manifest: { model: { name: "x", @@ -199,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 @@ -213,6 +219,7 @@ describe("AgentsClient", () => { await expect(async () => { return await client.agents.create({ + description: "x", manifest: { model: { name: "x", @@ -226,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 @@ -240,6 +247,7 @@ describe("AgentsClient", () => { await expect(async () => { return await client.agents.create({ + description: "x", manifest: { model: { name: "x", @@ -261,6 +269,7 @@ describe("AgentsClient", () => { subject_id: "subject_id", subject_type: "subject_type", }, + description: "description", id: "id", manifest: { instructions: "instructions", @@ -290,6 +299,7 @@ describe("AgentsClient", () => { subjectId: "subject_id", subjectType: "subject_type", }, + description: "description", id: "id", manifest: { instructions: "instructions", @@ -351,6 +361,7 @@ describe("AgentsClient", () => { subject_id: "subject_id", subject_type: "subject_type", }, + description: "description", id: "id", manifest: { instructions: "instructions", @@ -387,6 +398,7 @@ describe("AgentsClient", () => { subjectId: "subject_id", subjectType: "subject_type", }, + description: "description", id: "id", manifest: { instructions: "instructions", 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 */} - {/*