diff --git a/docs/openapi.json b/docs/openapi.json index 413b14456..b2cb676ec 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -190,7 +190,19 @@ { "properties": { "name": { - "$ref": "#/components/schemas/ResourceName" + "allOf": [ + { + "$ref": "#/components/schemas/ResourceName" + }, + { + "description": "Fully qualified name. Unique within a tenant." + } + ] + }, + "session_id": { + "description": "Mutable inline session updated atomically with this agent.", + "minLength": 1, + "type": "string" } }, "required": [ @@ -392,14 +404,13 @@ "additionalProperties": false, "properties": { "supported_reasoning_efforts": { - "description": "Supported reasoning-effort values for this provider.", + "description": "Supported reasoning-effort values for this provider", "items": { "$ref": "#/components/schemas/ReasoningEffort" }, "type": "array" }, "type": { - "description": "Custom provider type (catalog includes `custom`).", "enum": [ "custom" ], @@ -462,14 +473,7 @@ "additionalProperties": false, "properties": { "auth": { - "allOf": [ - { - "$ref": "#/components/schemas/ConfiguredMcpServerAuth" - }, - { - "description": "Optional default auth settings for the preset." - } - ] + "$ref": "#/components/schemas/ConfiguredMcpServerAuth" }, "logo": { "description": "URL of the MCP server logo asset.", @@ -477,7 +481,14 @@ "type": "string" }, "name": { - "$ref": "#/components/schemas/ResourceName" + "allOf": [ + { + "$ref": "#/components/schemas/ResourceName" + }, + { + "description": "Fully qualified name. Unique within a tenant." + } + ] }, "type": { "$ref": "#/components/schemas/McpServerType" @@ -514,7 +525,14 @@ "type": "string" }, "name": { - "$ref": "#/components/schemas/ResourceName" + "allOf": [ + { + "$ref": "#/components/schemas/ResourceName" + }, + { + "description": "Fully qualified name. Unique within a tenant." + } + ] }, "path": { "description": "Path to the skill directory within the repository. Omit to use the repository root.", @@ -551,30 +569,19 @@ "additionalProperties": false, "properties": { "logo": { - "description": "URL of the provider logo asset.", + "description": "URL of the provider logo asset", "format": "uri", "type": "string" }, "models": { - "description": "Preset models; empty on the `custom` sentinel.", + "description": "Preset models", "items": { "$ref": "#/components/schemas/ModelEntry" }, "type": "array" }, "type": { - "description": "Well-known provider type (catalog excludes `custom`).", - "enum": [ - "openai", - "anthropic", - "google-gemini", - "fireworks", - "zai", - "moonshot", - "alibaba", - "together" - ], - "type": "string" + "$ref": "#/components/schemas/CatalogWellKnownModelProviderType" } }, "required": [ @@ -583,6 +590,19 @@ ], "type": "object" }, + "CatalogWellKnownModelProviderType": { + "enum": [ + "openai", + "anthropic", + "google-gemini", + "fireworks", + "zai", + "moonshot", + "alibaba", + "together" + ], + "type": "string" + }, "ChatCompletionChunkDeltaToolCall": { "properties": { "function": { @@ -702,13 +722,27 @@ "additionalProperties": false, "properties": { "auth": { - "$ref": "#/components/schemas/ConfiguredMcpServerAuth" + "allOf": [ + { + "$ref": "#/components/schemas/ConfiguredMcpServerAuth" + }, + { + "description": "Optional auth settings. Omit when the server needs no credentials." + } + ] }, "auth_status": { "$ref": "#/components/schemas/McpAuthStatus" }, "name": { - "$ref": "#/components/schemas/ResourceName" + "allOf": [ + { + "$ref": "#/components/schemas/ResourceName" + }, + { + "description": "Fully qualified name. Unique within a tenant." + } + ] }, "type": { "$ref": "#/components/schemas/McpServerType" @@ -728,7 +762,7 @@ "type": "object" }, "ConfiguredMcpServerAuth": { - "description": "Optional auth settings. Omit when the server needs no credentials.", + "description": "Optional default auth settings for the preset.", "discriminator": { "mapping": { "dcr": "#/components/schemas/McpServerDcrAuth", @@ -783,6 +817,11 @@ "properties": { "data": { "$ref": "#/components/schemas/Agent" + }, + "session_updated_at": { + "description": "Updated session timestamp when `session_id` was supplied.", + "format": "date-time", + "type": "string" } }, "required": [ @@ -852,7 +891,14 @@ "type": "array" }, "name": { - "$ref": "#/components/schemas/ResourceName" + "allOf": [ + { + "$ref": "#/components/schemas/ResourceName" + }, + { + "description": "Fully qualified name. Unique within a tenant." + } + ] }, "type": { "enum": [ @@ -862,7 +908,6 @@ } }, "required": [ - "auth", "models", "type", "name", @@ -925,7 +970,7 @@ "description": "Daytona authentication credentials.", "properties": { "api_key": { - "description": "Daytona API key.", + "description": "Daytona API key. Responses are redacted; on PUT, a real value sets/rotates and a redacted value keeps the stored key.", "minLength": 1, "type": "string" } @@ -1400,7 +1445,7 @@ }, "ListSessionsOrder": { "default": "desc", - "description": "Sort sessions by creation time. Defaults to \"desc\".", + "description": "Sort sessions by `updated_at`. Defaults to \"desc\".", "enum": [ "asc", "desc" @@ -1725,6 +1770,7 @@ }, "McpAuthStatus": { "additionalProperties": false, + "description": "Auth state for the calling user.", "properties": { "authorization_url": { "description": "When auth is required, this contains the URL to redirect the user to for authorization.", @@ -1746,6 +1792,41 @@ ], "type": "object" }, + "McpServerAuthPublic": { + "description": "Auth mechanism when configured (no secrets). Omit when the server needs no credentials.", + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "type": { + "enum": [ + "dcr" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "type": { + "enum": [ + "header" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + } + ] + }, "McpServerDcrAuth": { "additionalProperties": false, "properties": { @@ -1770,7 +1851,7 @@ "minLength": 1, "type": "string" }, - "description": "HTTP headers sent on each request to this MCP server.", + "description": "Request headers for this MCP server. Responses are redacted; on PUT, a real value sets/rotates and a redacted value keeps the stored secret for that header name.", "type": "object" }, "type": { @@ -1791,10 +1872,24 @@ "additionalProperties": false, "properties": { "auth": { - "$ref": "#/components/schemas/ConfiguredMcpServerAuth" + "allOf": [ + { + "$ref": "#/components/schemas/ConfiguredMcpServerAuth" + }, + { + "description": "Optional auth settings. Omit when the server needs no credentials." + } + ] }, "name": { - "$ref": "#/components/schemas/ResourceName" + "allOf": [ + { + "$ref": "#/components/schemas/ResourceName" + }, + { + "description": "Fully qualified name. Unique within a tenant." + } + ] }, "type": { "$ref": "#/components/schemas/McpServerType" @@ -1815,8 +1910,21 @@ "McpServerReadEntry": { "additionalProperties": false, "properties": { + "auth": { + "$ref": "#/components/schemas/McpServerAuthPublic" + }, + "auth_status": { + "$ref": "#/components/schemas/McpAuthStatus" + }, "name": { - "$ref": "#/components/schemas/ResourceName" + "allOf": [ + { + "$ref": "#/components/schemas/ResourceName" + }, + { + "description": "Fully qualified name. Unique within a tenant." + } + ] }, "url": { "description": "URL of the remote MCP server.", @@ -1826,7 +1934,8 @@ }, "required": [ "name", - "url" + "url", + "auth_status" ], "type": "object" }, @@ -1876,11 +1985,15 @@ }, "properties": { "$ref": "#/components/schemas/ModelProperties" + }, + "provider": { + "$ref": "#/components/schemas/ModelListProvider" } }, "required": [ "name", "model_id", + "provider", "properties" ], "type": "object" @@ -1894,14 +2007,7 @@ "type": "string" }, "name": { - "allOf": [ - { - "$ref": "#/components/schemas/ResourceName" - }, - { - "description": "Internal identifier; forms the fully qualified name `name/model_name`." - } - ] + "$ref": "#/components/schemas/ResourceName" }, "properties": { "$ref": "#/components/schemas/ModelProperties" @@ -1914,6 +2020,21 @@ ], "type": "object" }, + "ModelListProvider": { + "additionalProperties": false, + "description": "Owning configured provider.", + "properties": { + "name": { + "description": "Configured provider resource name; matches the FQN prefix of `name`.", + "minLength": 1, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, "ModelMessageDeltaEvent": { "properties": { "content": { @@ -2241,7 +2362,7 @@ "description": "Provider authentication credentials.", "properties": { "api_key": { - "description": "API key used to authenticate with the provider.", + "description": "Provider API key. Responses are redacted; on PUT, a real value sets/rotates and a redacted value keeps the stored key.", "minLength": 1, "type": "string" } @@ -2345,6 +2466,11 @@ "properties": { "data": { "$ref": "#/components/schemas/Agent" + }, + "session_updated_at": { + "description": "Updated session timestamp when `session_id` was supplied.", + "format": "date-time", + "type": "string" } }, "required": [ @@ -2463,7 +2589,7 @@ "type": "object" }, "ResourceName": { - "description": "Fully qualified name. Unique within a tenant.", + "description": "Internal identifier; forms the fully qualified name `name/model_name`.", "maxLength": 64, "minLength": 2, "pattern": "^[a-z](?:[a-z0-9._-]{0,62}[a-z0-9])$", @@ -2837,7 +2963,14 @@ "additionalProperties": false, "properties": { "name": { - "$ref": "#/components/schemas/ResourceName" + "allOf": [ + { + "$ref": "#/components/schemas/ResourceName" + }, + { + "description": "Fully qualified name. Unique within a tenant." + } + ] } }, "required": [ @@ -2965,7 +3098,14 @@ "type": "string" }, "name": { - "$ref": "#/components/schemas/ResourceName" + "allOf": [ + { + "$ref": "#/components/schemas/ResourceName" + }, + { + "description": "Fully qualified name. Unique within a tenant." + } + ] }, "path": { "description": "Path to the skill directory within the repository. Omit to use the repository root.", @@ -3023,7 +3163,14 @@ "type": "string" }, "name": { - "$ref": "#/components/schemas/ResourceName" + "allOf": [ + { + "$ref": "#/components/schemas/ResourceName" + }, + { + "description": "Fully qualified name. Unique within a tenant." + } + ] } }, "required": [ @@ -3874,47 +4021,22 @@ ] }, "UpdateAgentRequest": { - "description": "Complete agent definition used inline on a session or saved as a named agent.", - "properties": { - "config": { - "$ref": "#/components/schemas/RuntimeConfig" - }, - "instructions": { - "description": "Optional system prompt — the agent's role, behavior, and constraints.", - "type": "string" - }, - "mcp_servers": { - "description": "Optional MCP servers attached by configured name.", - "items": { - "$ref": "#/components/schemas/MCPServer" - }, - "type": "array" - }, - "messages": { - "description": "Optional seed user messages injected at the start of every session.", - "items": { - "$ref": "#/components/schemas/AgentSpecUserMessage" - }, - "type": "array" - }, - "model": { - "$ref": "#/components/schemas/AgentSpecModel" - }, - "response_format": { - "$ref": "#/components/schemas/ResponseFormat" + "allOf": [ + { + "$ref": "#/components/schemas/AgentSpec" }, - "skills": { - "description": "Optional name-only skill references. Requires `config.sandbox.enabled: true`.", - "items": { - "$ref": "#/components/schemas/SkillNameRef" + { + "properties": { + "session_id": { + "description": "Mutable inline session updated atomically with this agent.", + "minLength": 1, + "type": "string" + } }, - "type": "array" + "type": "object" } - }, - "required": [ - "model" ], - "type": "object" + "description": "Complete agent definition used inline on a session or saved as a named agent." }, "UpdateSessionRequest": { "additionalProperties": false, @@ -4154,6 +4276,26 @@ }, "description": "Invalid request body or unknown model/MCP/skill refs." }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "The current user does not own the supplied session." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "The supplied session was not found." + }, "409": { "content": { "application/json": { @@ -4172,7 +4314,7 @@ } } }, - "description": "The agent spec is valid but requires a capability this server does not provide (e.g. sandbox or skills)." + "description": "The agent spec requires an unavailable capability (e.g. sandbox or skills), or `session_id` is not a mutable inline session." } }, "summary": "Create an agent", @@ -4326,6 +4468,16 @@ }, "description": "Invalid request body or unknown model/MCP/skill refs." }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "The current user does not own the supplied session." + }, "404": { "content": { "application/json": { @@ -4334,7 +4486,7 @@ } } }, - "description": "Agent not found." + "description": "Agent not found, or the supplied session was not found." }, "422": { "content": { @@ -4344,7 +4496,7 @@ } } }, - "description": "The agent spec is valid but requires a capability this server does not provide (e.g. sandbox or skills)." + "description": "The agent spec requires an unavailable capability (e.g. sandbox or skills), or `session_id` is not a mutable inline session." } }, "summary": "Update an agent", @@ -4531,19 +4683,163 @@ "x-fern-sdk-method-name": "get_capabilities" } }, - "/api/v1/mcp-servers": { + "/api/v1/catalog/mcp-servers": { "get": { - "description": "MCP servers as a slim name/url list for the composer. No auth or auth_status.", + "description": "Shipped MCP server presets (discovery-only). Copy into PUT /settings/mcp-servers to configure.", "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListAvailableMcpServersResponse" + "$ref": "#/components/schemas/GetMcpServerCatalogResponse" } } }, - "description": "All MCP servers (chat projection)." + "description": "Shipped MCP server presets." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Not authenticated." + } + }, + "summary": "Get the MCP catalog", + "tags": [ + "Catalog" + ], + "x-fern-sdk-group-name": [ + "catalog", + "mcpServers" + ], + "x-fern-sdk-method-name": "list" + } + }, + "/api/v1/catalog/model-providers": { + "get": { + "description": "Shipped model-provider presets (discovery-only). Copy into PUT /settings/model-providers to configure. Includes a `custom` sentinel with `supported_reasoning_efforts`.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetModelProviderCatalogResponse" + } + } + }, + "description": "Shipped model-provider presets." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Not authenticated." + } + }, + "summary": "Get the model catalog", + "tags": [ + "Catalog" + ], + "x-fern-sdk-group-name": [ + "catalog", + "modelProviders" + ], + "x-fern-sdk-method-name": "list" + } + }, + "/api/v1/catalog/sandbox-providers": { + "get": { + "description": "Shipped sandbox-provider presets (discovery-only). Copy into PUT /settings/sandbox-providers to configure.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSandboxProviderCatalogResponse" + } + } + }, + "description": "Shipped sandbox-provider presets." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Not authenticated." + } + }, + "summary": "Get the sandbox provider catalog", + "tags": [ + "Catalog" + ], + "x-fern-sdk-group-name": [ + "catalog", + "sandboxProviders" + ], + "x-fern-sdk-method-name": "list" + } + }, + "/api/v1/catalog/skills": { + "get": { + "description": "Shipped skill presets (discovery-only). Copy into PUT /settings/skills to configure.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSkillCatalogResponse" + } + } + }, + "description": "Shipped skill presets." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Not authenticated." + } + }, + "summary": "Get the skill catalog", + "tags": [ + "Catalog" + ], + "x-fern-sdk-group-name": [ + "catalog", + "skills" + ], + "x-fern-sdk-method-name": "list" + } + }, + "/api/v1/mcp-servers": { + "get": { + "description": "MCP servers as a slim name/url list for the composer. No auth or auth_status.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListAvailableMcpServersResponse" + } + } + }, + "description": "All MCP servers (chat projection)." }, "401": { "content": { @@ -4865,7 +5161,7 @@ } }, { - "description": "Sort sessions by creation time. Defaults to \"desc\".", + "description": "Sort sessions by `updated_at`. Defaults to \"desc\".", "in": "query", "name": "order", "required": false, @@ -4874,12 +5170,13 @@ } }, { - "description": "Opaque token from a previous response `next_page_token`.", + "description": "Opaque keyset cursor from a previous response `next_page_token`.", "in": "query", "name": "page_token", "required": false, "schema": { - "description": "Opaque token from a previous response `next_page_token`.", + "description": "Opaque keyset cursor from a previous response `next_page_token`.", + "minLength": 1, "type": "string" } }, @@ -6028,7 +6325,7 @@ }, "/api/v1/settings/mcp-servers": { "get": { - "description": "All MCP servers with nested auth_status (settings / admin projection).", + "description": "All MCP servers with nested auth_status (settings / admin projection). Header auth values are redacted.", "responses": { "200": { "content": { @@ -6038,7 +6335,7 @@ } } }, - "description": "All MCP servers." + "description": "All MCP servers" }, "401": { "content": { @@ -6072,7 +6369,7 @@ "x-fern-sdk-method-name": "list" }, "put": { - "description": "Full upsert keyed by `name`: creates the server or replaces its manifest. Does not start DCR or modify stored oauth_server / oauth_client columns.", + "description": "Create or replace by `name`. Does not start DCR or change oauth client columns. Header secrets: real value sets/rotates; redacted keeps existing (400 if none).", "requestBody": { "content": { "application/json": { @@ -6092,7 +6389,7 @@ } } }, - "description": "The saved MCP server with stub auth_status." + "description": "The saved MCP server with auth_status" }, "400": { "content": { @@ -6102,7 +6399,7 @@ } } }, - "description": "Invalid request body." + "description": "Invalid request body, or redacted header secret with no stored value to keep." }, "422": { "content": { @@ -6126,55 +6423,9 @@ "x-fern-sdk-method-name": "upsert" } }, - "/api/v1/settings/mcp-servers/catalog": { - "get": { - "description": "MCP server presets shipped with the server (mcp-catalog.yaml). Discovery-only: copy an entry into PUT /settings/mcp-servers to configure it.", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GetMcpServerCatalogResponse" - } - } - }, - "description": "The shipped catalog, verbatim." - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RequestErrorResponse" - } - } - }, - "description": "OIDC is configured and the request has no valid session cookie." - }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RequestErrorResponse" - } - } - }, - "description": "OIDC is configured and the caller is authenticated but not an admin." - } - }, - "summary": "Get the MCP catalog", - "tags": [ - "MCP Servers" - ], - "x-fern-sdk-group-name": [ - "settings", - "mcpServers" - ], - "x-fern-sdk-method-name": "catalog" - } - }, "/api/v1/settings/mcp-servers/{name}": { "get": { - "description": "A single MCP server by name, with nested auth_status (settings / admin projection).", + "description": "A single MCP server by name, with nested auth_status (settings / admin projection). Header auth values are redacted.", "parameters": [ { "description": "MCP server name.", @@ -6197,7 +6448,7 @@ } } }, - "description": "The MCP server." + "description": "The MCP server" }, "404": { "content": { @@ -6312,7 +6563,7 @@ } } }, - "description": "All configured model providers." + "description": "All configured model providers" }, "401": { "content": { @@ -6346,7 +6597,7 @@ "x-fern-sdk-method-name": "list" }, "put": { - "description": "Full upsert: creates the provider or replaces its entire configuration (models included). The key is the returned `name`, which every type but `custom` takes from its own `type`, so each is limited to one configured provider and a repeat call replaces it; only `custom` providers are named by the caller.", + "description": "Create or replace a provider (models included). Well-known types use `type` as `name` (one each); `custom` is named by the caller. `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none).", "requestBody": { "content": { "application/json": { @@ -6366,7 +6617,7 @@ } } }, - "description": "The saved provider." + "description": "The saved provider" }, "400": { "content": { @@ -6376,7 +6627,7 @@ } } }, - "description": "Invalid request body." + "description": "Invalid request body, or redacted API key with no stored secret to keep." } }, "summary": "Create or replace a model provider", @@ -6390,55 +6641,9 @@ "x-fern-sdk-method-name": "upsert" } }, - "/api/v1/settings/model-providers/catalog": { - "get": { - "description": "Provider and model presets shipped with the server (model-catalog.yaml). Discovery-only: an entry becomes a PUT /settings/model-providers body once the catalog-only `logo` and `name` are dropped and `auth` is added. Includes a `custom` sentinel with `supported_reasoning_efforts` for the custom-provider form (not configurable from catalog).", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GetModelProviderCatalogResponse" - } - } - }, - "description": "The shipped catalog, verbatim." - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RequestErrorResponse" - } - } - }, - "description": "OIDC is configured and the request has no valid session cookie." - }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RequestErrorResponse" - } - } - }, - "description": "OIDC is configured and the caller is authenticated but not an admin." - } - }, - "summary": "Get the model catalog", - "tags": [ - "Model Providers" - ], - "x-fern-sdk-group-name": [ - "settings", - "modelProviders" - ], - "x-fern-sdk-method-name": "catalog" - } - }, "/api/v1/settings/sandbox-providers": { "get": { - "description": "The single configured sandbox provider for this tenant.", + "description": "The single configured sandbox provider for this tenant. `auth.api_key` is redacted.", "responses": { "200": { "content": { @@ -6472,7 +6677,7 @@ "x-fern-sdk-method-name": "get" }, "put": { - "description": "Upserts the single sandbox provider for this tenant: creates it or replaces its entire configuration.", + "description": "Upserts the single sandbox provider for this tenant: creates it or replaces its entire configuration. `auth.api_key`: real value sets/rotates; redacted keeps existing (400 if none).", "requestBody": { "content": { "application/json": { @@ -6502,7 +6707,7 @@ } } }, - "description": "Invalid request body." + "description": "Invalid request body, or redacted API key with no stored secret to keep." } }, "summary": "Create or replace the sandbox provider", @@ -6516,52 +6721,6 @@ "x-fern-sdk-method-name": "upsert" } }, - "/api/v1/settings/sandbox-providers/catalog": { - "get": { - "description": "Sandbox provider presets shipped with the server (sandbox-catalog.yaml). Discovery-only: copy an entry into PUT /settings/sandbox-providers to configure it.", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GetSandboxProviderCatalogResponse" - } - } - }, - "description": "The shipped catalog, verbatim." - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RequestErrorResponse" - } - } - }, - "description": "OIDC is configured and the request has no valid session cookie." - }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RequestErrorResponse" - } - } - }, - "description": "OIDC is configured and the caller is authenticated but not an admin." - } - }, - "summary": "Get the sandbox provider catalog", - "tags": [ - "Sandbox Providers" - ], - "x-fern-sdk-group-name": [ - "settings", - "sandboxProviders" - ], - "x-fern-sdk-method-name": "catalog" - } - }, "/api/v1/settings/skills": { "get": { "description": "All configured skills with full manifests (settings / admin projection).", @@ -6652,52 +6811,6 @@ "x-fern-sdk-method-name": "upsert" } }, - "/api/v1/settings/skills/catalog": { - "get": { - "description": "Skill presets shipped with the server (skill-catalog.yaml). Discovery-only: copy an entry into PUT /settings/skills to configure it.", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GetSkillCatalogResponse" - } - } - }, - "description": "The shipped catalog, verbatim." - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RequestErrorResponse" - } - } - }, - "description": "OIDC is configured and the request has no valid session cookie." - }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RequestErrorResponse" - } - } - }, - "description": "OIDC is configured and the caller is authenticated but not an admin." - } - }, - "summary": "Get the skill catalog", - "tags": [ - "Skills" - ], - "x-fern-sdk-group-name": [ - "settings", - "skills" - ], - "x-fern-sdk-method-name": "catalog" - } - }, "/api/v1/skills": { "get": { "description": "Configured skills as a slim name/description list for the composer.", diff --git a/packages/frontend/src/harnessBuilderServer.ts b/packages/frontend/src/harnessBuilderServer.ts index 3e4762401..3608dc0d6 100644 --- a/packages/frontend/src/harnessBuilderServer.ts +++ b/packages/frontend/src/harnessBuilderServer.ts @@ -92,16 +92,21 @@ export function createHarnessBuilderServer( return filtered.slice(offset, offset + limit).map(toLibraryEntry); }, - async saveAgent({ agentName, agentSpec, intent }) { + async saveAgent({ agentName, agentSpec, intent, sessionId }) { const manifest = toHarnessAgentSpec(agentSpec); + const request = sessionId === undefined ? manifest : { ...manifest, sessionId }; if (intent === 'update') { - const { data } = await client.agents.list(); - const existing = data.find(agent => agent.name === agentName); - await client.agents.update(agentName, manifest); - return existing === undefined ? {} : { agentId: existing.id }; + const updated = await client.agents.update(agentName, request); + return { + agentId: updated.data.id, + ...(updated.sessionUpdatedAt === undefined ? {} : { sessionUpdatedAt: updated.sessionUpdatedAt }), + }; } - const created = await client.agents.create({ name: agentName, ...manifest }); - return { agentId: created.data.id }; + const created = await client.agents.create({ name: agentName, ...request }); + return { + agentId: created.data.id, + ...(created.sessionUpdatedAt === undefined ? {} : { sessionUpdatedAt: created.sessionUpdatedAt }), + }; }, }; } diff --git a/packages/frontend/tests/harnessBuilderServer.test.ts b/packages/frontend/tests/harnessBuilderServer.test.ts index f08f1f27a..1f7627c76 100644 --- a/packages/frontend/tests/harnessBuilderServer.test.ts +++ b/packages/frontend/tests/harnessBuilderServer.test.ts @@ -210,7 +210,7 @@ describe('harnessBuilderServer', () => { ); }); - it('saveAgent creates when the name is new', async () => { + it('saveAgent creates directly from explicit intent', async () => { const requests: { method: string; url: string; body?: unknown }[] = []; const fetchMock: typeof fetch = async (input, init) => { const url = input instanceof Request ? input.url : String(input); @@ -219,6 +219,7 @@ describe('harnessBuilderServer', () => { requests.push({ method, url, body: JSON.parse(init.body) }); return Response.json({ data: { id: 'agt_new', name: 'saved-agent', model: { name: 'test/model' } }, + session_updated_at: '2026-08-12T08:00:00.000Z', }); } return new Response(`Unexpected request: ${method} ${url}`, { status: 500 }); @@ -232,26 +233,27 @@ describe('harnessBuilderServer', () => { skills: [{ name: 'review' }], }, intent: 'create', + sessionId: 'draft-1', }); - assert.deepEqual(result, { agentId: 'agt_new' }); + assert.deepEqual(result, { + agentId: 'agt_new', + sessionUpdatedAt: '2026-08-12T08:00:00.000Z', + }); + assert.equal(requests.length, 1); assert.deepEqual(requests.at(-1)?.body, { name: 'saved-agent', model: { name: 'test/model' }, skills: [{ name: 'review' }], + session_id: 'draft-1', }); }); - it('saveAgent updates when the name already exists', async () => { + it('saveAgent updates directly from explicit intent', async () => { const requests: { method: string; url: string; body?: unknown }[] = []; const fetchMock: typeof fetch = async (input, init) => { const url = input instanceof Request ? input.url : String(input); const method = init?.method ?? 'GET'; - if (url.endsWith('/api/v1/agents') && method === 'GET') { - return Response.json({ - data: [{ id: 'agt_1', name: 'writer', model: { name: 'test/model' } }], - }); - } if (url.endsWith('/api/v1/agents/writer') && method === 'PUT' && typeof init?.body === 'string') { requests.push({ method, url, body: JSON.parse(init.body) }); return Response.json({ @@ -261,6 +263,7 @@ describe('harnessBuilderServer', () => { model: { name: 'test/model' }, instructions: 'Write release notes.', }, + session_updated_at: '2026-08-12T09:00:00.000Z', }); } return new Response(`Unexpected request: ${method} ${url}`, { status: 500 }); @@ -274,13 +277,18 @@ describe('harnessBuilderServer', () => { instructions: 'Write release notes.', }, intent: 'update', + sessionId: 'draft-2', }); - assert.deepEqual(result, { agentId: 'agt_1' }); + assert.deepEqual(result, { + agentId: 'agt_1', + sessionUpdatedAt: '2026-08-12T09:00:00.000Z', + }); assert.equal(requests.length, 1); assert.deepEqual(requests[0]?.body, { model: { name: 'test/model' }, instructions: 'Write release notes.', + session_id: 'draft-2', }); }); }); diff --git a/packages/sdk/src/api/resources/agents/client/requests/AgentWriteRequest.ts b/packages/sdk/src/api/resources/agents/client/requests/AgentWriteRequest.ts index 5e3b9d022..fab569288 100644 --- a/packages/sdk/src/api/resources/agents/client/requests/AgentWriteRequest.ts +++ b/packages/sdk/src/api/resources/agents/client/requests/AgentWriteRequest.ts @@ -14,4 +14,6 @@ import type * as TrueForge from "../../../../index.js"; export interface AgentWriteRequest extends TrueForge.AgentSpec { /** Fully qualified name. Unique within a tenant. */ name: string; + /** Mutable inline session updated atomically with this agent. */ + sessionId?: string; } diff --git a/packages/sdk/src/api/resources/agents/client/requests/UpdateAgentRequest.ts b/packages/sdk/src/api/resources/agents/client/requests/UpdateAgentRequest.ts index 2e211e85e..6a10c362f 100644 --- a/packages/sdk/src/api/resources/agents/client/requests/UpdateAgentRequest.ts +++ b/packages/sdk/src/api/resources/agents/client/requests/UpdateAgentRequest.ts @@ -20,6 +20,8 @@ export interface UpdateAgentRequest { messages?: TrueForge.AgentSpecUserMessage[]; model: TrueForge.AgentSpecModel; responseFormat?: TrueForge.ResponseFormat; + /** Mutable inline session updated atomically with this agent. */ + sessionId?: string; /** Optional name-only skill references. Requires `config.sandbox.enabled: true`. */ skills?: TrueForge.SkillNameRef[]; } diff --git a/packages/sdk/src/api/types/CreateAgentResponse.ts b/packages/sdk/src/api/types/CreateAgentResponse.ts index 726309082..56d3d6e8a 100644 --- a/packages/sdk/src/api/types/CreateAgentResponse.ts +++ b/packages/sdk/src/api/types/CreateAgentResponse.ts @@ -4,4 +4,6 @@ import type * as TrueForge from "../index.js"; export interface CreateAgentResponse { data: TrueForge.Agent; + /** Updated session timestamp when `sessionId` was supplied. */ + sessionUpdatedAt?: string; } diff --git a/packages/sdk/src/api/types/PutAgentResponse.ts b/packages/sdk/src/api/types/PutAgentResponse.ts index 4f7912db4..f3b5af97c 100644 --- a/packages/sdk/src/api/types/PutAgentResponse.ts +++ b/packages/sdk/src/api/types/PutAgentResponse.ts @@ -4,4 +4,6 @@ import type * as TrueForge from "../index.js"; export interface PutAgentResponse { data: TrueForge.Agent; + /** Updated session timestamp when `sessionId` was supplied. */ + sessionUpdatedAt?: string; } diff --git a/packages/sdk/src/serialization/resources/agents/client/requests/AgentWriteRequest.ts b/packages/sdk/src/serialization/resources/agents/client/requests/AgentWriteRequest.ts index 8e58d0e05..65878509c 100644 --- a/packages/sdk/src/serialization/resources/agents/client/requests/AgentWriteRequest.ts +++ b/packages/sdk/src/serialization/resources/agents/client/requests/AgentWriteRequest.ts @@ -11,11 +11,13 @@ export const AgentWriteRequest: core.serialization.Schema< > = core.serialization .object({ name: core.serialization.string(), + sessionId: core.serialization.property("session_id", core.serialization.string().optional()), }) .extend(AgentSpec); export declare namespace AgentWriteRequest { export interface Raw extends AgentSpec.Raw { name: string; + session_id?: string | null; } } diff --git a/packages/sdk/src/serialization/resources/agents/client/requests/UpdateAgentRequest.ts b/packages/sdk/src/serialization/resources/agents/client/requests/UpdateAgentRequest.ts index f99132ad1..5627ff703 100644 --- a/packages/sdk/src/serialization/resources/agents/client/requests/UpdateAgentRequest.ts +++ b/packages/sdk/src/serialization/resources/agents/client/requests/UpdateAgentRequest.ts @@ -20,6 +20,7 @@ export const UpdateAgentRequest: core.serialization.Schema< messages: core.serialization.list(AgentSpecUserMessage).optional(), model: AgentSpecModel, responseFormat: core.serialization.property("response_format", ResponseFormat.optional()), + sessionId: core.serialization.property("session_id", core.serialization.string().optional()), skills: core.serialization.list(SkillNameRef).optional(), }); @@ -31,6 +32,7 @@ export declare namespace UpdateAgentRequest { messages?: AgentSpecUserMessage.Raw[] | null; model: AgentSpecModel.Raw; response_format?: ResponseFormat.Raw | null; + session_id?: string | null; skills?: SkillNameRef.Raw[] | null; } } diff --git a/packages/sdk/src/serialization/types/CreateAgentResponse.ts b/packages/sdk/src/serialization/types/CreateAgentResponse.ts index 633d4cb5b..751839ce4 100644 --- a/packages/sdk/src/serialization/types/CreateAgentResponse.ts +++ b/packages/sdk/src/serialization/types/CreateAgentResponse.ts @@ -10,10 +10,12 @@ export const CreateAgentResponse: core.serialization.ObjectSchema< TrueForge.CreateAgentResponse > = core.serialization.object({ data: Agent, + sessionUpdatedAt: core.serialization.property("session_updated_at", core.serialization.string().optional()), }); export declare namespace CreateAgentResponse { export interface Raw { data: Agent.Raw; + session_updated_at?: string | null; } } diff --git a/packages/sdk/src/serialization/types/PutAgentResponse.ts b/packages/sdk/src/serialization/types/PutAgentResponse.ts index 4465fc9b6..4e69cdae3 100644 --- a/packages/sdk/src/serialization/types/PutAgentResponse.ts +++ b/packages/sdk/src/serialization/types/PutAgentResponse.ts @@ -10,10 +10,12 @@ export const PutAgentResponse: core.serialization.ObjectSchema< TrueForge.PutAgentResponse > = core.serialization.object({ data: Agent, + sessionUpdatedAt: core.serialization.property("session_updated_at", core.serialization.string().optional()), }); export declare namespace PutAgentResponse { export interface Raw { data: Agent.Raw; + session_updated_at?: string | null; } } diff --git a/packages/sdk/tests/wire/agents.test.ts b/packages/sdk/tests/wire/agents.test.ts index 244c0bf2e..e92e8788c 100644 --- a/packages/sdk/tests/wire/agents.test.ts +++ b/packages/sdk/tests/wire/agents.test.ts @@ -76,7 +76,7 @@ describe("AgentsClient", () => { test("create (1)", async () => { const server = mockServerPool.createServer(); const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); - const rawRequestBody = { model: { name: "name" }, name: "name" }; + const rawRequestBody = { model: { name: "name" }, name: "name", session_id: "session_id" }; const rawResponseBody = { data: { config: { iteration_limit: 1, sandbox: { enabled: true } }, @@ -89,6 +89,7 @@ describe("AgentsClient", () => { id: "id", name: "name", }, + session_updated_at: "2026-08-12T08:00:00.000Z", }; server @@ -105,6 +106,7 @@ describe("AgentsClient", () => { name: "name", }, name: "name", + sessionId: "session_id", }); expect(response).toEqual({ data: { @@ -140,6 +142,7 @@ describe("AgentsClient", () => { id: "id", name: "name", }, + sessionUpdatedAt: "2026-08-12T08:00:00.000Z", }); }); @@ -333,7 +336,7 @@ describe("AgentsClient", () => { test("update (1)", async () => { const server = mockServerPool.createServer(); const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); - const rawRequestBody = { model: { name: "name" } }; + const rawRequestBody = { model: { name: "name" }, session_id: "session_id" }; const rawResponseBody = { data: { config: { iteration_limit: 1, sandbox: { enabled: true } }, @@ -346,6 +349,7 @@ describe("AgentsClient", () => { id: "id", name: "name", }, + session_updated_at: "2026-08-12T09:00:00.000Z", }; server @@ -361,6 +365,7 @@ describe("AgentsClient", () => { model: { name: "name", }, + sessionId: "session_id", }); expect(response).toEqual({ data: { @@ -396,6 +401,7 @@ describe("AgentsClient", () => { id: "id", name: "name", }, + sessionUpdatedAt: "2026-08-12T09:00:00.000Z", }); }); diff --git a/packages/trueforge-ui-sdk/example/yarn.lock b/packages/trueforge-ui-sdk/example/yarn.lock index 323841010..f7f5903b9 100644 --- a/packages/trueforge-ui-sdk/example/yarn.lock +++ b/packages/trueforge-ui-sdk/example/yarn.lock @@ -1425,10 +1425,10 @@ "@tailwindcss/oxide" "4.3.3" tailwindcss "4.3.3" -"@truefoundry/assistant-ui-runtime@0.1.13": - version "0.1.13" - resolved "https://registry.yarnpkg.com/@truefoundry/assistant-ui-runtime/-/assistant-ui-runtime-0.1.13.tgz#64191bf904a31c5a32f45c3314ebe86117f2e57a" - integrity sha512-pmY4Fvm5tZmX+uhsr3RegTeY3yYrdQdqaqYV5QzPZyrOPloboVPO07jbcZ2/D7S33622Ug0drhKGvWHpYTMTDw== +"@truefoundry/assistant-ui-runtime@0.1.14": + version "0.1.14" + resolved "https://registry.yarnpkg.com/@truefoundry/assistant-ui-runtime/-/assistant-ui-runtime-0.1.14.tgz#c77b2f41ae260b965c16d72dc1b9f73ddde66813" + integrity sha512-xdFob3v6KdhUVFrDuw1OAcX6o8lsnT+kQAo82gzCgXbg3YvdIs4cDWRswZdCI83FriRieYtMc8kZPjrRysJSkQ== dependencies: "@assistant-ui/core" "^0.2.22" "@assistant-ui/store" "^0.2.21" @@ -2859,6 +2859,11 @@ parse5@^7.0.0: dependencies: entities "^6.0.0" +partial-json@^0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/partial-json/-/partial-json-0.1.7.tgz#b735a89edb3e25f231a3c4caeaae71dc9f578605" + integrity sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA== + picocolors@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" diff --git a/packages/trueforge-ui-sdk/src/atoms/SaveAgentButton.tsx b/packages/trueforge-ui-sdk/src/atoms/SaveAgentButton.tsx index 61ca6ad0d..bd4ba7c07 100644 --- a/packages/trueforge-ui-sdk/src/atoms/SaveAgentButton.tsx +++ b/packages/trueforge-ui-sdk/src/atoms/SaveAgentButton.tsx @@ -1,154 +1,450 @@ 'use client'; -import { useTrueFoundryAgentSpec } from '@truefoundry/assistant-ui-runtime'; -import { useId, useState, type FormEvent } from 'react'; - +import { + useTrueFoundryAdoptAgentSpec, + useTrueFoundryAgentSpec, + useTrueFoundryFlushAgentSpec, +} from '@truefoundry/assistant-ui-runtime'; +import { useId, useMemo, useRef, useState } from 'react'; import { useSaveAgentVisible } from '../hooks/useChatChromeActionsVisible.js'; -import { useOptionalServer } from '../server/ServerContext.js'; +import { Icon } from '../icons/Icon.js'; +import { useOptionalServer, useServerCapabilities } from '../server/ServerContext.js'; import { useOptionalShellMode } from '../server/ShellModeContext.js'; import type { AgentSpec } from '../server/types.js'; import { getErrorMessage } from '../utils/getErrorMessage.js'; +import { readAgentCapabilities, withAgentCapabilities } from './draft/agentCapabilities.js'; +import { DraftCapabilitiesPanel } from './draft/DraftCapabilitiesPanel.js'; +import { DraftCatalogProvider, useDraftCatalog } from './draft/DraftCatalogProvider.js'; +import { CatalogRow, ConnectorConnectButton, isUnauthenticatedDcrConnector } from './draft/DraftCompositeSelector.js'; +import { displayModelLabel, DraftModelCatalogPanel } from './draft/DraftModelCatalogPanel.js'; +import { modelPatchWithReasoningEffort } from './draft/reasoningEffort.js'; import { auiButtonClass } from './lib/buttonClasses.js'; import { auiInputClass } from './lib/inputClasses.js'; import { CenteredModal } from './primitives/CenteredModal.js'; -const inputClassName = auiInputClass('disabled:opacity-50'); +type SaveIntent = 'create' | 'update'; +type Editor = 'model' | 'mcp' | 'skills'; +type EditableMount = { id: string; name: string; value: object }; + +function editableMountsFromSpec(value: unknown): EditableMount[] { + if (!Array.isArray(value)) return []; + const mounts: EditableMount[] = []; + for (const item of value) { + if (typeof item !== 'object' || item === null) continue; + const name = Reflect.get(item, 'name'); + if (typeof name !== 'string') continue; + const id = Reflect.get(item, 'id'); + mounts.push({ id: typeof id === 'string' ? id : name, name, value: item }); + } + return mounts; +} + +function SummarySection({ + icon, + title, + value, + onEdit, + disabled, +}: { + icon: string; + title: string; + value: string; + onEdit: () => void; + disabled: boolean; +}) { + return ( +
+ +
+

{title}

+

{value}

+
+ +
+ ); +} + +export type SaveAgentButtonProps = { + disabled?: boolean; + className?: string; + children?: string; +}; -export function SaveAgentButton() { +export function SaveAgentButton({ disabled = false, className, children = 'Save Agent' }: SaveAgentButtonProps) { + return ( + + + {children} + + + ); +} + +function SaveAgentButtonContent({ + disabled, + className, + children, +}: { + disabled: boolean; + className?: string; + children: string; +}) { + const { agentSpec, draftSessionId } = useTrueFoundryAgentSpec(); + const agentSpecRef = useRef(agentSpec); + agentSpecRef.current = agentSpec; + const flushAgentSpec = useTrueFoundryFlushAgentSpec(); + const adoptAgentSpec = useTrueFoundryAdoptAgentSpec(); + const builder = useOptionalServer(); const shell = useOptionalShellMode(); - const server = useOptionalServer(); - const { agentSpec } = useTrueFoundryAgentSpec(); + const catalog = useDraftCatalog(); + const serverCapabilities = useServerCapabilities(); + const modelListId = useId(); const visible = useSaveAgentVisible(); const [open, setOpen] = useState(false); + const [editor, setEditor] = useState(null); + const [intent, setIntent] = useState('create'); const [name, setName] = useState(''); - const [instructions, setInstructions] = useState(''); + const [draftSpec, setDraftSpec] = useState(null); + const [modelQuery, setModelQuery] = useState(''); + const [search, setSearch] = useState(''); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); - const nameId = useId(); - const instructionsId = useId(); - - if (!visible || shell?.mode.status !== 'active' || !shell.mode.isMutable || server == null) return null; - const editingAgentName = shell.mode.agentName ?? shell.mode.agentId; - const isUpdate = editingAgentName != null && editingAgentName !== ''; - const draftSpec = shell.mode.agentSpec ?? { model: { name: 'openai-main/gpt-4.1' } }; - // Runtime AgentSpec is structurally compatible; cast across package boundary. - const specToSave: AgentSpec = (agentSpec as AgentSpec | undefined) ?? draftSpec; + const mcpMounts = useMemo(() => editableMountsFromSpec(draftSpec?.mcpServers), [draftSpec?.mcpServers]); + const skillMounts = useMemo(() => editableMountsFromSpec(draftSpec?.skills), [draftSpec?.skills]); - const reset = () => { - setName(''); - setInstructions(''); + const close = () => { + if (saving) return; + setEditor(null); + setOpen(false); + setDraftSpec(null); setError(null); - setSaving(false); }; - const handleOpenChange = (next: boolean) => { - if (next) { - setName(editingAgentName ?? ''); - setInstructions(specToSave.instructions ?? draftSpec.instructions ?? ''); - } else { - reset(); - } - setOpen(next); + const show = async () => { + if (agentSpecRef.current === null || builder === null) return; + setError(null); + catalog.ensureLoaded(); + await flushAgentSpec(); + const latestAgentSpec = agentSpecRef.current; + if (latestAgentSpec === null) return; + const currentName = shell?.mode.status === 'active' ? (shell.mode.agentName ?? shell.mode.agentId ?? '') : ''; + setIntent(currentName ? 'update' : 'create'); + setName(currentName); + setDraftSpec({ + ...latestAgentSpec, + model: { + ...latestAgentSpec.model, + params: latestAgentSpec.model.params ? { ...latestAgentSpec.model.params } : undefined, + }, + mcpServers: latestAgentSpec.mcpServers?.map(item => ({ ...item })), + skills: latestAgentSpec.skills?.map(item => ({ ...item })), + config: latestAgentSpec.config ? { ...latestAgentSpec.config } : undefined, + }); + setOpen(true); }; - const handleSubmit = async (event: FormEvent) => { - event.preventDefault(); - const agentName = name.trim(); - if (!agentName || saving) return; - + const save = async () => { + if (builder === null || draftSpec === null) return; + const normalizedName = name.trim(); + if (!normalizedName || !draftSpec.model.name.trim()) return; setSaving(true); setError(null); - const trimmedInstructions = instructions.trim(); - const savedSpec: AgentSpec = { - ...specToSave, - instructions: trimmedInstructions || undefined, - }; try { - await server.saveAgent({ - agentName, - agentSpec: savedSpec, - intent: isUpdate ? 'update' : 'create', + const result = await builder.saveAgent({ + agentName: normalizedName, + agentSpec: draftSpec, + intent, + sessionId: draftSessionId, }); - // Same draft chat continues as editable agent — do not remount via selectLibraryAgent. - shell.bindMutableAgent({ - agentId: agentName, - agentName, - agentSpec: savedSpec, + adoptAgentSpec({ agentSpec: draftSpec, updatedAt: result.sessionUpdatedAt }); + shell?.bindMutableAgent({ + agentId: result.agentId ?? normalizedName, + agentName: normalizedName, + agentSpec: draftSpec, }); - shell.invalidateAgentsList(); - handleOpenChange(false); - } catch (err: unknown) { - setError(getErrorMessage(err, isUpdate ? 'Failed to update agent.' : 'Failed to save agent.')); + shell?.invalidateAgentsList(); + setOpen(false); + setDraftSpec(null); + setEditor(null); + } catch (caught) { + setError(getErrorMessage(caught, 'Could not save agent')); + } finally { setSaving(false); } }; - const triggerLabel = isUpdate ? 'Update Agent' : 'Save agent'; - const modalTitle = isUpdate ? 'Update Agent' : 'Save agent'; - const modalDescription = isUpdate - ? 'Save changes to this agent in the Agents library' - : 'Reuse this setup later from the Agents library'; - const submitLabel = saving ? (isUpdate ? 'Updating…' : 'Saving…') : isUpdate ? 'Update' : 'Save'; + const filteredConnectors = catalog.connectors.filter(item => + `${item.name} ${item.description ?? ''}`.toLowerCase().includes(search.trim().toLowerCase()), + ); + const filteredSkills = catalog.skills.filter(item => + `${item.name} ${item.description ?? ''}`.toLowerCase().includes(search.trim().toLowerCase()), + ); + const isUpdateMode = + shell?.mode.status === 'active' && + shell.mode.isMutable && + (shell.mode.agentName !== undefined || shell.mode.agentId !== undefined); + const triggerLabel = isUpdateMode && children === 'Save Agent' ? 'Update Agent' : children; + + if (!visible) return null; return ( <> !next && close()} + title={intent === 'create' ? 'Save agent' : 'Update agent'} + description="Review the configuration before saving." + className="md:max-w-lg" + aria-label={intent === 'create' ? 'Save agent' : 'Update agent'} > -
void handleSubmit(e)}> -
- - setName(e.target.value)} - placeholder="release-notes-writer" - autoFocus={!isUpdate} - autoComplete="off" - disabled={saving || isUpdate} - className={`${inputClassName} h-9`} - /> + {draftSpec ? ( +
+
+ + +