diff --git a/cookbook/python/src/main.py b/cookbook/python/src/main.py index 44dca07..65b1872 100644 --- a/cookbook/python/src/main.py +++ b/cookbook/python/src/main.py @@ -20,7 +20,7 @@ async def main(): try: # Create agent agent = await client.agents.create( - name="Dad Jokes Bot", + name="dad-jokes-bot-py", system_prompt="You are a dad joke expert. Tell one short, cheesy dad joke.", ) print(f"Created agent: {agent.id}") diff --git a/cookbook/python/src/weather_tools.py b/cookbook/python/src/weather_tools.py index 046998f..ef5d42b 100644 --- a/cookbook/python/src/weather_tools.py +++ b/cookbook/python/src/weather_tools.py @@ -67,7 +67,7 @@ async def main(): try: # Create agent with tool-aware system prompt agent = await client.agents.create( - name="Weather Assistant", + name="weather-assistant-py", system_prompt=SYSTEM_PROMPT, ) print(f"Created agent: {agent.id}") diff --git a/cookbook/rust/src/main.rs b/cookbook/rust/src/main.rs index 82945d1..0c018e8 100644 --- a/cookbook/rust/src/main.rs +++ b/cookbook/rust/src/main.rs @@ -15,7 +15,7 @@ async fn main() -> Result<(), Box> { let agent = client .agents() .create( - "Dad Jokes Bot", + "dad-jokes-bot-rs", "You are a dad joke expert. Tell one short, cheesy dad joke.", ) .await?; diff --git a/cookbook/rust/src/weather_tools.rs b/cookbook/rust/src/weather_tools.rs index aee8360..2a8feaf 100644 --- a/cookbook/rust/src/weather_tools.rs +++ b/cookbook/rust/src/weather_tools.rs @@ -53,7 +53,7 @@ async fn main() -> Result<(), Box> { // Create agent with tool-aware system prompt let agent = client .agents() - .create("Weather Assistant", SYSTEM_PROMPT) + .create("weather-assistant-rs", SYSTEM_PROMPT) .await?; println!("Created agent: {}", agent.id); diff --git a/cookbook/typescript/src/main.ts b/cookbook/typescript/src/main.ts index 3bf47a5..1a981c6 100644 --- a/cookbook/typescript/src/main.ts +++ b/cookbook/typescript/src/main.ts @@ -14,7 +14,7 @@ async function main() { // Create agent const agent = await client.agents.create({ - name: "Dad Jokes Bot", + name: "dad-jokes-bot-ts", systemPrompt: "You are a dad joke expert. Tell one short, cheesy dad joke.", }); console.log(`Created agent: ${agent.id}`); diff --git a/cookbook/typescript/src/weather_tools.ts b/cookbook/typescript/src/weather_tools.ts index 02b1d5c..d282adf 100644 --- a/cookbook/typescript/src/weather_tools.ts +++ b/cookbook/typescript/src/weather_tools.ts @@ -66,7 +66,7 @@ async function main() { // Create agent with tool-aware system prompt const agent = await client.agents.create({ - name: "Weather Assistant", + name: "weather-assistant-ts", systemPrompt: SYSTEM_PROMPT, }); console.log(`Created agent: ${agent.id}`); diff --git a/openapi/openapi.json b/openapi/openapi.json index cd043a4..2d4e0f6 100644 --- a/openapi/openapi.json +++ b/openapi/openapi.json @@ -35,15 +35,58 @@ "null" ] } + }, + { + "name": "include_archived", + "in": "query", + "description": "Include archived agents. Deleted agents never appear in lists.", + "required": false, + "schema": { + "type": [ + "boolean", + "null" + ] + } + }, + { + "name": "offset", + "in": "query", + "description": "Pagination offset (default 0).", + "required": false, + "schema": { + "type": [ + "integer", + "null" + ], + "format": "int32", + "default": 0, + "minimum": 0 + } + }, + { + "name": "limit", + "in": "query", + "description": "Maximum items to return (default 20, max 100).", + "required": false, + "schema": { + "type": [ + "integer", + "null" + ], + "format": "int32", + "default": 20, + "maximum": 100, + "minimum": 1 + } } ], "responses": { "200": { - "description": "List of agents", + "description": "Paginated list of agents", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListResponse_Agent" + "$ref": "#/components/schemas/PaginatedResponse_Agent" } } } @@ -109,7 +152,7 @@ "agents" ], "summary": "POST /v1/agents/import - Import agent from Markdown, YAML, or JSON", - "description": "Accepts agent definition in multiple formats:\n- Markdown with YAML front matter (if starts with ---)\n- Pure YAML\n- Pure JSON\n- Plain text (treated as system prompt, name auto-generated)", + "description": "Accepts agent definition in multiple formats:\n- Markdown with YAML front matter (if starts with ---)\n- Pure YAML\n- Pure JSON\n- Plain text (treated as system prompt, name auto-generated)\n\nIf the file contains an `id` field and an agent with that ID already exists,\nthe agent is updated (upsert). Returns 201 on create, 200 on update.", "operationId": "import_agent", "requestBody": { "content": { @@ -122,6 +165,16 @@ "required": true }, "responses": { + "200": { + "description": "Agent updated via import", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Agent" + } + } + } + }, "201": { "description": "Agent imported successfully", "content": { @@ -202,13 +255,14 @@ "tags": [ "agents" ], - "summary": "GET /v1/agents/{agent_id} - Get agent by ID", + "summary": "GET /v1/agents/{agent_id} - Get agent by ID or name", + "description": "Accepts either an agent ID (e.g. `agent_01933b5a...`) or a\nname (e.g. `customer-support`). Names are resolved within the caller's org.", "operationId": "get_agent", "parameters": [ { "name": "agent_id", "in": "path", - "description": "Agent ID (prefixed, e.g., agt_...)", + "description": "Agent ID (prefixed) or name", "required": true, "schema": { "type": "string" @@ -242,13 +296,13 @@ "agents" ], "summary": "PUT /v1/agents/{agent_id} - Create or update agent (upsert)", - "description": "If agent with this ID exists, update it. If not, create it.\nReturns 201 on create, 200 on update.", + "description": "Accepts either an agent ID (e.g. `agent_01933b5a...`) or a\nname (e.g. `customer-support`). If the agent exists, update it; if not,\ncreate it. Returns 201 on create, 200 on update.", "operationId": "upsert_agent", "parameters": [ { "name": "agent_id", "in": "path", - "description": "Agent ID (format: agent_{32-hex})", + "description": "Agent ID (prefixed) or name", "required": true, "schema": { "type": "string" @@ -287,7 +341,7 @@ } }, "400": { - "description": "Invalid agent ID or input exceeds allowed limits", + "description": "Invalid input", "content": { "application/json": { "schema": { @@ -1249,49 +1303,53 @@ } } }, - "/v1/images": { + "/v1/harnesses": { "get": { "tags": [ - "images" + "harnesses" ], - "summary": "GET /v1/images - List images", - "operationId": "list_images", + "summary": "GET /v1/harnesses", + "operationId": "list_harnesses", "parameters": [ { - "name": "limit", + "name": "search", "in": "query", - "description": "Max items to return (default 50)", + "description": "Search by name or description (case-insensitive substring match).", "required": false, "schema": { - "type": "integer", - "format": "int64" + "type": [ + "string", + "null" + ] } }, { - "name": "offset", + "name": "include_archived", "in": "query", - "description": "Items to skip", + "description": "Include archived harnesses. Deleted harnesses never appear in lists.", "required": false, "schema": { - "type": "integer", - "format": "int64" + "type": [ + "boolean", + "null" + ] } } ], "responses": { "200": { - "description": "List of images", + "description": "List of harnesses", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ImageInfo" - } + "$ref": "#/components/schemas/ListResponse_Harness" } } } }, + "403": { + "description": "Forbidden" + }, "500": { "description": "Internal server error" } @@ -1299,28 +1357,15 @@ }, "post": { "tags": [ - "images" - ], - "summary": "POST /v1/images - Upload an image", - "operationId": "upload_image", - "parameters": [ - { - "name": "session_id", - "in": "query", - "description": "Optional: session ID stored as metadata for tracking (not required for upload)", - "required": false, - "schema": { - "type": "string", - "format": "uuid" - } - } + "harnesses" ], + "summary": "POST /v1/harnesses", + "operationId": "create_harness", "requestBody": { - "description": "Multipart form data with 'file' field", "content": { - "multipart/form-data": { + "application/json": { "schema": { - "type": "string" + "$ref": "#/components/schemas/CreateHarnessRequest" } } }, @@ -1328,36 +1373,124 @@ }, "responses": { "201": { - "description": "Image uploaded successfully", + "description": "Harness created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ImageUploadResponse" + "$ref": "#/components/schemas/Harness" } } } }, "400": { - "description": "Invalid request (bad format, size exceeded, etc.)" + "description": "Invalid input", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, "500": { - "description": "Internal server error" + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } } } }, - "/v1/images/{image_id}": { + "/v1/harnesses/config": { "get": { "tags": [ - "images" + "harnesses" ], - "summary": "GET /v1/images/{image_id} - Get image (returns binary data)", - "operationId": "get_image", + "summary": "GET /v1/harnesses/config", + "description": "Returns which harness policies the caller satisfies.\nUI uses this to show/hide controls (e.g. delete button).", + "operationId": "harness_config", + "responses": { + "200": { + "description": "Resource config for harnesses", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceConfigResponse" + } + } + } + } + } + } + }, + "/v1/harnesses/preview": { + "post": { + "tags": [ + "harnesses" + ], + "summary": "POST /v1/harnesses/preview", + "operationId": "preview_harness", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PreviewHarnessRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Harness preview generated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HarnessPreviewResponse" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/v1/harnesses/{harness_id}": { + "get": { + "tags": [ + "harnesses" + ], + "summary": "GET /v1/harnesses/{harness_id}", + "description": "Accepts either a harness ID (e.g. `harness_01933b5a...`) or a\nname (e.g. `generic`). The virtual name `default` resolves to the org's\nconfigured default harness. Names are resolved within the caller's org.", + "operationId": "get_harness", "parameters": [ { - "name": "image_id", + "name": "harness_id", "in": "path", - "description": "Image ID (prefixed, e.g., img_...)", + "description": "Harness ID (prefixed) or name", "required": true, "schema": { "type": "string" @@ -1366,16 +1499,20 @@ ], "responses": { "200": { - "description": "Image binary data", + "description": "Harness found", "content": { - "image/*": {} + "application/json": { + "schema": { + "$ref": "#/components/schemas/Harness" + } + } } }, - "400": { - "description": "Invalid image ID" + "403": { + "description": "Forbidden" }, "404": { - "description": "Image not found" + "description": "Harness not found" }, "500": { "description": "Internal server error" @@ -1384,15 +1521,15 @@ }, "delete": { "tags": [ - "images" + "harnesses" ], - "summary": "DELETE /v1/images/{image_id} - Delete an image", - "operationId": "delete_image", + "summary": "DELETE /v1/harnesses/{harness_id}", + "operationId": "delete_harness", "parameters": [ { - "name": "image_id", + "name": "harness_id", "in": "path", - "description": "Image ID (prefixed, e.g., img_...)", + "description": "Harness ID (prefixed)", "required": true, "schema": { "type": "string" @@ -1401,55 +1538,376 @@ ], "responses": { "204": { - "description": "Image deleted" + "description": "Harness archived" }, "400": { - "description": "Invalid image ID" + "description": "Invalid harness ID" + }, + "403": { + "description": "Forbidden" }, "404": { - "description": "Image not found" + "description": "Harness not found" }, "500": { "description": "Internal server error" } } - } - }, - "/v1/images/{image_id}/thumbnail": { - "get": { + }, + "patch": { "tags": [ - "images" + "harnesses" ], - "summary": "GET /v1/images/{image_id}/thumbnail - Get image thumbnail", - "operationId": "get_thumbnail", + "summary": "PATCH /v1/harnesses/{harness_id}", + "operationId": "update_harness", "parameters": [ { - "name": "image_id", + "name": "harness_id", "in": "path", - "description": "Image ID (prefixed, e.g., img_...)", + "description": "Harness ID (prefixed)", "required": true, "schema": { "type": "string" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateHarnessRequest" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "Thumbnail binary data", + "description": "Harness updated", "content": { - "image/jpeg": {} + "application/json": { + "schema": { + "$ref": "#/components/schemas/Harness" + } + } } }, "400": { - "description": "Invalid image ID" + "description": "Invalid input", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, "404": { - "description": "Image not found or no thumbnail" + "description": "Harness not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, "500": { - "description": "Internal server error" - } - } + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/v1/harnesses/{harness_id}/copy": { + "post": { + "tags": [ + "harnesses" + ], + "summary": "POST /v1/harnesses/{harness_id}/copy - Copy a harness", + "description": "Creates a new harness with the same configuration as the source harness.\nThe new harness's name will be \"{original name} (copy)\".", + "operationId": "copy_harness", + "parameters": [ + { + "name": "harness_id", + "in": "path", + "description": "Source harness ID to copy", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "Harness copied successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Harness" + } + } + } + }, + "400": { + "description": "Invalid harness ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Source harness not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/v1/images": { + "get": { + "tags": [ + "images" + ], + "summary": "GET /v1/images - List images", + "operationId": "list_images", + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "Max items to return (default 50)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "offset", + "in": "query", + "description": "Items to skip", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "List of images", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageInfo" + } + } + } + } + }, + "500": { + "description": "Internal server error" + } + } + }, + "post": { + "tags": [ + "images" + ], + "summary": "POST /v1/images - Upload an image", + "operationId": "upload_image", + "parameters": [ + { + "name": "session_id", + "in": "query", + "description": "Optional: session ID stored as metadata for tracking (not required for upload)", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "Multipart form data with 'file' field", + "content": { + "multipart/form-data": { + "schema": { + "type": "string" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Image uploaded successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImageUploadResponse" + } + } + } + }, + "400": { + "description": "Invalid request (bad format, size exceeded, etc.)" + }, + "500": { + "description": "Internal server error" + } + } + } + }, + "/v1/images/{image_id}": { + "get": { + "tags": [ + "images" + ], + "summary": "GET /v1/images/{image_id} - Get image (returns binary data)", + "operationId": "get_image", + "parameters": [ + { + "name": "image_id", + "in": "path", + "description": "Image ID (prefixed, e.g., img_...)", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Image binary data", + "content": { + "image/*": {} + } + }, + "400": { + "description": "Invalid image ID" + }, + "404": { + "description": "Image not found" + }, + "500": { + "description": "Internal server error" + } + } + }, + "delete": { + "tags": [ + "images" + ], + "summary": "DELETE /v1/images/{image_id} - Delete an image", + "operationId": "delete_image", + "parameters": [ + { + "name": "image_id", + "in": "path", + "description": "Image ID (prefixed, e.g., img_...)", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Image deleted" + }, + "400": { + "description": "Invalid image ID" + }, + "404": { + "description": "Image not found" + }, + "500": { + "description": "Internal server error" + } + } + } + }, + "/v1/images/{image_id}/thumbnail": { + "get": { + "tags": [ + "images" + ], + "summary": "GET /v1/images/{image_id}/thumbnail - Get image thumbnail", + "operationId": "get_thumbnail", + "parameters": [ + { + "name": "image_id", + "in": "path", + "description": "Image ID (prefixed, e.g., img_...)", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Thumbnail binary data", + "content": { + "image/jpeg": {} + } + }, + "400": { + "description": "Invalid image ID" + }, + "404": { + "description": "Image not found or no thumbnail" + }, + "500": { + "description": "Internal server error" + } + } } }, "/v1/llm-models": { @@ -1908,6 +2366,9 @@ "400": { "description": "Invalid provider ID" }, + "404": { + "description": "Provider not found" + }, "500": { "description": "Internal error" } @@ -1933,6 +2394,18 @@ "null" ] } + }, + { + "name": "include_archived", + "in": "query", + "description": "Include archived MCP servers. Deleted MCP servers never appear in lists.", + "required": false, + "schema": { + "type": [ + "boolean", + "null" + ] + } } ], "responses": { @@ -2445,6 +2918,50 @@ } } }, + "/v1/sessions/chat": { + "post": { + "tags": [ + "sessions" + ], + "summary": "POST /v1/sessions/chat - Get or create global chat session", + "description": "Returns the user's singleton global chat session. Creates one if it doesn't exist.\nUses the Platform Chat harness and tags for per-user singleton management.", + "operationId": "get_or_create_chat_session", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/GetOrCreateChatSessionRequest" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Chat session returned", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Session" + } + } + } + }, + "401": { + "description": "Authentication required" + }, + "500": { + "description": "Internal server error" + } + } + } + }, "/v1/sessions/{session_id}": { "get": { "tags": [ @@ -2906,26 +3423,67 @@ ], "responses": { "200": { - "description": "Events list", - "headers": { - "X-Total-Count": { - "schema": { - "type": "integer", - "format": "int64" - }, - "description": "Total non-delta event count for the session (only present when limit is used)" - } - }, + "description": "Events list", + "headers": { + "X-Total-Count": { + "schema": { + "type": "integer", + "format": "int64" + }, + "description": "Total non-delta event count for the session (only present when limit is used)" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListResponse_Event" + } + } + } + }, + "400": { + "description": "Invalid session ID or invalid event type filter" + }, + "404": { + "description": "Session not found" + }, + "500": { + "description": "Internal server error" + } + } + } + }, + "/v1/sessions/{session_id}/export": { + "get": { + "tags": [ + "sessions" + ], + "summary": "Export session messages as a JSONL file", + "description": "Returns all materialized messages (user, agent) as newline-delimited JSON.\nDelta events are excluded. Each line is a complete JSON object representing one message.\nThe response includes `Content-Disposition: attachment` for browser download.", + "operationId": "export_session_jsonl", + "parameters": [ + { + "name": "session_id", + "in": "path", + "description": "Session ID (prefixed, e.g., session_...)", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "JSONL file with one message per line", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ListResponse_Event" - } - } + "application/x-ndjson": {} } }, "400": { - "description": "Invalid session ID or invalid event type filter" + "description": "Invalid ID format" + }, + "403": { + "description": "Forbidden" }, "404": { "description": "Session not found" @@ -3253,7 +3811,260 @@ { "name": "path", "in": "path", - "description": "File path", + "description": "File path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateFileRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Updated successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionFile" + } + } + } + }, + "400": { + "description": "Invalid session ID or cannot modify readonly file or directory" + }, + "404": { + "description": "Not found" + }, + "500": { + "description": "Internal server error" + } + } + }, + "post": { + "tags": [ + "filesystem" + ], + "summary": "POST /fs/*path - Create file or directory", + "operationId": "create_path", + "parameters": [ + { + "name": "session_id", + "in": "path", + "description": "Session ID (prefixed, e.g., sess_...)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "path", + "in": "path", + "description": "File or directory path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateFileRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionFile" + } + } + } + }, + "400": { + "description": "Invalid session ID or request" + }, + "409": { + "description": "Already exists" + }, + "500": { + "description": "Internal server error" + } + } + }, + "delete": { + "tags": [ + "filesystem" + ], + "summary": "DELETE /fs/*path - Delete file or directory", + "operationId": "delete_path", + "parameters": [ + { + "name": "session_id", + "in": "path", + "description": "Session ID (prefixed, e.g., sess_...)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "path", + "in": "path", + "description": "File or directory path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "recursive", + "in": "query", + "description": "Delete recursively", + "required": false, + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "Deleted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteResponse" + } + } + } + }, + "400": { + "description": "Invalid session ID or directory not empty" + }, + "403": { + "description": "Cannot delete readonly file or directory containing readonly files" + }, + "500": { + "description": "Internal server error" + } + } + } + }, + "/v1/sessions/{session_id}/git/branches": { + "post": { + "tags": [ + "Session Git" + ], + "summary": "POST /v1/sessions/{session_id}/git/branches", + "operationId": "create_branch", + "parameters": [ + { + "name": "session_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateBranchRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Branch created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse" + } + } + } + }, + "400": { + "description": "Invalid request" + } + } + } + }, + "/v1/sessions/{session_id}/git/branches/{name}": { + "delete": { + "tags": [ + "Session Git" + ], + "summary": "DELETE /v1/sessions/{session_id}/git/branches/{name}", + "operationId": "delete_branch", + "parameters": [ + { + "name": "session_id", + "in": "path", + "description": "Session ID", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "name", + "in": "path", + "description": "Branch name", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Branch deleted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse" + } + } + } + }, + "404": { + "description": "Branch not found" + } + } + } + }, + "/v1/sessions/{session_id}/git/commit": { + "post": { + "tags": [ + "Session Git" + ], + "summary": "POST /v1/sessions/{session_id}/git/commit", + "operationId": "commit", + "parameters": [ + { + "name": "session_id", + "in": "path", "required": true, "schema": { "type": "string" @@ -3264,146 +4075,176 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateFileRequest" + "$ref": "#/components/schemas/CommitRequest" } } }, "required": true }, "responses": { - "200": { - "description": "Updated successfully", + "201": { + "description": "Commit created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SessionFile" + "$ref": "#/components/schemas/CommitResult" } } } }, "400": { - "description": "Invalid session ID or cannot modify readonly file or directory" + "description": "Invalid request" }, "404": { - "description": "Not found" - }, - "500": { - "description": "Internal server error" + "description": "Session not found" } } - }, - "post": { + } + }, + "/v1/sessions/{session_id}/git/diff": { + "get": { "tags": [ - "filesystem" + "Session Git" ], - "summary": "POST /fs/*path - Create file or directory", - "operationId": "create_path", + "summary": "GET /v1/sessions/{session_id}/git/diff", + "operationId": "diff", "parameters": [ { "name": "session_id", "in": "path", - "description": "Session ID (prefixed, e.g., sess_...)", + "description": "Session ID", "required": true, "schema": { "type": "string" } }, { - "name": "path", - "in": "path", - "description": "File or directory path", + "name": "oid", + "in": "query", + "description": "Commit OID to diff", "required": true, "schema": { "type": "string" } + }, + { + "name": "base", + "in": "query", + "description": "Base commit OID (default: parent)", + "required": false, + "schema": { + "type": "string" + } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateFileRequest" - } - } - }, - "required": true - }, "responses": { - "201": { - "description": "Created successfully", + "200": { + "description": "Diff result", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SessionFile" + "$ref": "#/components/schemas/GitDiff" } } } }, "400": { - "description": "Invalid session ID or request" - }, - "409": { - "description": "Already exists" + "description": "Invalid commit OID" }, - "500": { - "description": "Internal server error" + "404": { + "description": "Commit not found" } } - }, - "delete": { + } + }, + "/v1/sessions/{session_id}/git/log": { + "get": { "tags": [ - "filesystem" + "Session Git" ], - "summary": "DELETE /fs/*path - Delete file or directory", - "operationId": "delete_path", + "summary": "GET /v1/sessions/{session_id}/git/log", + "operationId": "log", "parameters": [ { "name": "session_id", "in": "path", - "description": "Session ID (prefixed, e.g., sess_...)", + "description": "Session ID", "required": true, "schema": { "type": "string" } }, { - "name": "path", - "in": "path", - "description": "File or directory path", - "required": true, + "name": "ref", + "in": "query", + "description": "Ref to start from (default: HEAD)", + "required": false, "schema": { "type": "string" } }, { - "name": "recursive", + "name": "limit", "in": "query", - "description": "Delete recursively", + "description": "Max commits (default: 50)", "required": false, "schema": { - "type": "boolean" + "type": "integer", + "minimum": 0 } } ], "responses": { "200": { - "description": "Deleted", + "description": "Commit log", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeleteResponse" + "type": "array", + "items": { + "$ref": "#/components/schemas/GitCommitInfo" + } } } } }, - "400": { - "description": "Invalid session ID or directory not empty" - }, - "403": { - "description": "Cannot delete readonly file or directory containing readonly files" - }, - "500": { - "description": "Internal server error" + "404": { + "description": "Session or ref not found" + } + } + } + }, + "/v1/sessions/{session_id}/git/refs": { + "get": { + "tags": [ + "Session Git" + ], + "summary": "GET /v1/sessions/{session_id}/git/refs", + "operationId": "list_refs", + "parameters": [ + { + "name": "session_id", + "in": "path", + "description": "Session ID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "List of refs", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GitRefInfo" + } + } + } + } } } } @@ -3857,6 +4698,18 @@ "null" ] } + }, + { + "name": "include_archived", + "in": "query", + "description": "Include archived skills. Deleted skills never appear in lists.", + "required": false, + "schema": { + "type": [ + "boolean", + "null" + ] + } } ], "responses": { @@ -4323,6 +5176,14 @@ "updated_at" ], "properties": { + "archived_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Timestamp when the agent was archived." + }, "capabilities": { "type": "array", "items": { @@ -4343,6 +5204,14 @@ "description": "Default LLM model ID for this agent.\nCan be overridden at the session level.", "example": "model_01933b5a00007000800000000000001" }, + "deleted_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Timestamp when the agent was deleted." + }, "description": { "type": [ "string", @@ -4350,6 +5219,13 @@ ], "description": "Human-readable description of what the agent does." }, + "display_name": { + "type": [ + "string", + "null" + ], + "description": "Human-readable display name shown in UI (e.g. \"Customer Support Agent\").\nFalls back to `name` when absent." + }, "id": { "type": "string", "description": "External identifier (agent_<32-hex>). Shown as \"id\" in API.\nClient-supplied or auto-generated.", @@ -4362,9 +5238,28 @@ }, "description": "Starter files copied into each new session for this agent." }, + "max_iterations": { + "type": [ + "integer", + "null" + ], + "description": "Maximum number of LLM iterations per turn for this agent.", + "minimum": 0 + }, "name": { "type": "string", - "description": "Display name of the agent." + "description": "Name, unique per org (e.g. \"customer-support\")." + }, + "network_access": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/NetworkAccessList", + "description": "Network access list controlling which hosts/URLs agent sessions can reach.\nMerged with harness and session layers (allowed: intersect, blocked: union)." + } + ] }, "status": { "$ref": "#/components/schemas/AgentStatus", @@ -4445,12 +5340,58 @@ }, "AgentStatus": { "type": "string", - "description": "Agent lifecycle status.\n- `active`: Agent is available for use\n- `archived`: Agent is soft-deleted and hidden from listings", + "description": "Agent lifecycle status.\n- `active`: Agent is available for use\n- `archived`: Agent is hidden from listings and cannot be modified or assigned\n- `deleted`: Agent is a tombstone kept only for historical references", "enum": [ "active", - "archived" + "archived", + "deleted" ] }, + "BudgetEventData": { + "type": "object", + "description": "Data for budget lifecycle events (warning, paused, exhausted, resumed).", + "required": [ + "budget_id", + "balance", + "limit", + "currency" + ], + "properties": { + "balance": { + "type": "number", + "format": "double", + "description": "Current remaining balance." + }, + "budget_id": { + "type": "string", + "description": "Budget that triggered this event." + }, + "currency": { + "type": "string", + "description": "Budget currency (e.g. \"usd\", \"tokens\")." + }, + "limit": { + "type": "number", + "format": "double", + "description": "Budget limit." + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "Human-readable message." + }, + "soft_limit": { + "type": [ + "number", + "null" + ], + "format": "double", + "description": "Soft limit threshold (present for warning/paused events)." + } + } + }, "BuiltinTool": { "type": "object", "description": "Built-in tool configuration\n\nNote: The `kind` field has been removed. Tools are now identified\nsolely by their `name` field, and execution happens via the ToolRegistry\nwhich looks up tools by name.", @@ -4482,6 +5423,10 @@ ], "description": "Human-readable display name for UI rendering (e.g., \"Get Current Time\" for `get_current_time`)" }, + "hints": { + "$ref": "#/components/schemas/ToolHints", + "description": "Semantic hints describing the tool's behavioral properties" + }, "name": { "type": "string", "description": "Tool name (used by LLM and for registry lookup)" @@ -4635,6 +5580,10 @@ ], "description": "Human-readable display name for UI rendering" }, + "hints": { + "$ref": "#/components/schemas/ToolHints", + "description": "Semantic hints describing the tool's behavioral properties" + }, "name": { "type": "string", "description": "Tool name (used by LLM and for correlation)" @@ -4648,22 +5597,112 @@ "type": "object", "description": "A single tool result from the client", "required": [ - "tool_call_id" + "tool_call_id" + ], + "properties": { + "error": { + "type": [ + "string", + "null" + ], + "description": "Error message if the tool failed" + }, + "result": { + "description": "Result value (JSON). Null if the tool failed." + }, + "tool_call_id": { + "type": "string", + "description": "Tool call ID (correlates with the tool call from tool.call_requested event)" + } + } + }, + "CommitRequest": { + "type": "object", + "description": "Request to create a commit", + "required": [ + "message" + ], + "properties": { + "author_email": { + "type": [ + "string", + "null" + ], + "description": "Author email (defaults to \"agent@everruns.local\")" + }, + "author_name": { + "type": [ + "string", + "null" + ], + "description": "Author name (defaults to \"Agent\")" + }, + "branch": { + "type": [ + "string", + "null" + ], + "description": "Branch name (defaults to \"refs/heads/main\"); short names like \"main\" are normalized" + }, + "message": { + "type": "string", + "description": "Commit message" + } + } + }, + "CommitResult": { + "type": "object", + "description": "Result of a commit operation", + "required": [ + "oid", + "tree_oid", + "objects_created" + ], + "properties": { + "objects_created": { + "type": "integer", + "minimum": 0 + }, + "oid": { + "type": "string" + }, + "tree_oid": { + "type": "string" + } + } + }, + "CompactionReason": { + "type": "string", + "description": "Reason why compaction was triggered.", + "enum": [ + "proactive_budget", + "request_too_large", + "manual" + ] + }, + "CompactionStepData": { + "type": "object", + "description": "A single step in a compaction cascade.", + "required": [ + "strategy", + "messages_after", + "duration_ms" ], "properties": { - "error": { - "type": [ - "string", - "null" - ], - "description": "Error message if the tool failed" + "duration_ms": { + "type": "integer", + "format": "int64", + "description": "Duration of this step in milliseconds.", + "minimum": 0 }, - "result": { - "description": "Result value (JSON). Null if the tool failed." + "messages_after": { + "type": "integer", + "description": "Number of messages after this step.", + "minimum": 0 }, - "tool_call_id": { + "strategy": { "type": "string", - "description": "Tool call ID (correlates with the tool call from tool.call_requested event)" + "description": "Strategy used in this step." } } }, @@ -4787,10 +5826,87 @@ ], "description": "A part of message content - can be text, image, image_file, tool_call, or tool_result\n\nThis is the canonical content part type used across the system.\nAPI layer enables the \"openapi\" feature to add ToSchema derive." }, + "ContextCompactedData": { + "type": "object", + "description": "Data for context.compacted event (compaction completed).", + "required": [ + "strategy_used", + "messages_before", + "messages_after", + "duration_ms" + ], + "properties": { + "duration_ms": { + "type": "integer", + "format": "int64", + "description": "Total duration of all compaction steps in milliseconds.", + "minimum": 0 + }, + "messages_after": { + "type": "integer", + "description": "Number of messages after compaction.", + "minimum": 0 + }, + "messages_before": { + "type": "integer", + "description": "Number of messages before compaction.", + "minimum": 0 + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CompactionStepData" + }, + "description": "Individual steps in the cascade." + }, + "strategy_used": { + "type": "string", + "description": "Combined strategy description (e.g., \"observation_masking+native\")." + } + } + }, + "ContextCompactingData": { + "type": "object", + "description": "Data for context.compacting event (compaction starting).", + "required": [ + "reason", + "strategy", + "messages_before" + ], + "properties": { + "messages_before": { + "type": "integer", + "description": "Number of messages before compaction.", + "minimum": 0 + }, + "reason": { + "$ref": "#/components/schemas/CompactionReason", + "description": "Why compaction was triggered." + }, + "strategy": { + "type": "string", + "description": "Strategy requested (may differ from strategy_used in the completed event)." + } + } + }, "Controls": { "type": "object", "description": "Runtime controls for message processing", "properties": { + "hints": { + "type": [ + "object", + "null" + ], + "description": "Generic client hints — arbitrary key-value pairs declared by the client.\nSession-level defaults are set at session creation; per-message values\noverride session hints key-by-key (shallow merge).\n\nExamples: `{\"setup_connection\": true, \"rich_media\": true}`" + }, + "locale": { + "type": [ + "string", + "null" + ], + "description": "Locale override for this message turn (BCP 47, e.g. `uk-UA`).\nOverrides the session locale for backend-authored strings and prompts." + }, "model_id": { "type": [ "string", @@ -4830,6 +5946,40 @@ } } }, + "CostTier": { + "type": "object", + "description": "A pricing tier that activates above a context token threshold.\nFor example, OpenAI charges higher rates for prompts exceeding 200K tokens.", + "required": [ + "above_tokens", + "input", + "output" + ], + "properties": { + "above_tokens": { + "type": "integer", + "format": "int32", + "description": "Context token threshold above which this tier applies" + }, + "cache_read": { + "type": [ + "number", + "null" + ], + "format": "double", + "description": "Cached read cost per million tokens (USD) for this tier, if supported" + }, + "input": { + "type": "number", + "format": "double", + "description": "Input cost per million tokens (USD) for this tier" + }, + "output": { + "type": "number", + "format": "double", + "description": "Output cost per million tokens (USD) for this tier" + } + } + }, "CreateAgentRequest": { "type": "object", "description": "Request to create a new agent", @@ -4871,6 +6021,14 @@ "description": "A human-readable description of what the agent does.", "example": "Handles customer inquiries and support tickets" }, + "display_name": { + "type": [ + "string", + "null" + ], + "description": "Human-readable display name shown in UI.\nFalls back to `name` when absent.", + "example": "Customer Support Agent" + }, "id": { "type": [ "string", @@ -4886,10 +6044,29 @@ }, "description": "Starter files copied into each new session for this agent." }, + "max_iterations": { + "type": [ + "integer", + "null" + ], + "description": "Maximum number of LLM iterations per turn for this agent.", + "minimum": 0 + }, "name": { "type": "string", - "description": "The name of the agent. Used for display purposes.", - "example": "Customer Support Agent" + "description": "Name, unique per org. Lowercase alphanumeric and hyphens.\nFormat: lowercase alphanumeric and hyphens (e.g. \"customer-support\").", + "example": "customer-support" + }, + "network_access": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/NetworkAccessList", + "description": "Network access list controlling which hosts/URLs this agent's sessions can reach.\nIf set, merged with harness and session layers (allowed: intersect, blocked: union)." + } + ] }, "system_prompt": { "type": "string", @@ -4916,6 +6093,24 @@ } } }, + "CreateBranchRequest": { + "type": "object", + "description": "Request to create a branch", + "required": [ + "name", + "commit_oid" + ], + "properties": { + "commit_oid": { + "type": "string", + "description": "Commit OID (hex) to point to" + }, + "name": { + "type": "string", + "description": "Branch name (e.g., \"feature-xyz\")" + } + } + }, "CreateDatabaseRequest": { "type": "object", "description": "Request body for creating a database.", @@ -4963,6 +6158,104 @@ } } }, + "CreateHarnessRequest": { + "type": "object", + "description": "Request to create a new harness", + "required": [ + "name", + "system_prompt" + ], + "properties": { + "capabilities": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AgentCapabilityConfig" + }, + "description": "Capabilities to enable with per-harness configuration.", + "example": [ + { + "config": {}, + "ref": "current_time" + }, + { + "config": {}, + "ref": "web_fetch" + } + ] + }, + "default_model_id": { + "type": [ + "string", + "null" + ], + "description": "Default LLM model ID for this harness. Lowest priority in model chain.", + "example": "model_01933b5a00007000800000000000001" + }, + "description": { + "type": [ + "string", + "null" + ], + "description": "Description of what the harness does.", + "example": "Research harness with planning and web capabilities" + }, + "display_name": { + "type": [ + "string", + "null" + ], + "description": "Human-readable display name shown in UI.", + "example": "Deep Research" + }, + "initial_files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InitialFile" + }, + "description": "Starter files copied into each new session for this harness." + }, + "name": { + "type": "string", + "description": "Name, unique per org. Lowercase alphanumeric and hyphens.", + "example": "deep-research" + }, + "network_access": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/NetworkAccessList", + "description": "Network access list controlling which hosts/URLs sessions can reach." + } + ] + }, + "parent_harness_id": { + "type": [ + "string", + "null" + ], + "description": "Optional parent harness to inherit from.", + "example": "harness_01933b5a000070008000000000000602" + }, + "system_prompt": { + "type": "string", + "description": "The system prompt defining the harness's base behavior.", + "example": "You are a research assistant with deep analytical capabilities." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Tags for organizing harnesses.", + "example": [ + "research", + "planning" + ] + } + } + }, "CreateLlmModelRequest": { "type": "object", "description": "Request to create a new LLM model for a provider", @@ -5054,6 +6347,17 @@ ], "description": "API key for authentication (optional)." }, + "auth_mode": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/McpServerAuthMode", + "description": "Authentication mode. Defaults to `api_key` when `api_key` is provided, otherwise `none`." + } + ] + }, "description": { "type": [ "string", @@ -5236,6 +6540,14 @@ "description": "ID of the agent to work in this session (optional, format: agent_{32-hex}).", "example": "agent_01933b5a00007000800000000000001" }, + "agent_identity_id": { + "type": [ + "string", + "null" + ], + "description": "Optional resident agent identity used for unattended/background execution.", + "example": "identity_01933b5a00007000800000000000001" + }, "capabilities": { "type": "array", "items": { @@ -5244,10 +6556,35 @@ "description": "Session-level capabilities (additive to agent capabilities).\nApplied after agent capabilities when building RuntimeAgent." }, "harness_id": { - "type": "string", - "description": "ID of the harness for this session (format: harness_{32-hex}).\nDefaults to the Generic harness if omitted (backward compat with older SDKs).", + "type": [ + "string", + "null" + ], + "description": "ID of the harness for this session (format: harness_{32-hex}).\nIf omitted, the org default harness is used. New orgs default that to Generic.\nMutually exclusive with `harness_name`.", "example": "harness_01933b5a00007000800000000000001" }, + "harness_name": { + "type": [ + "string", + "null" + ], + "description": "Harness name (e.g. \"generic\", \"deep-research\").\nAlternative to `harness_id` — looked up by name within the org.\nMutually exclusive with `harness_id`.", + "example": "generic" + }, + "hints": { + "type": [ + "object", + "null" + ], + "description": "Session-level client hints — arbitrary key-value pairs that tell the\nserver what the client can handle. These are defaults for every turn;\nper-message `controls.hints` override these key-by-key (shallow merge).\n\nExamples: `{\"setup_connection\": true, \"rich_media\": true}`" + }, + "initial_files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InitialFile" + }, + "description": "Session-level initial files (additive to agent initial_files).\nFiles with matching paths override agent/harness files; new paths are appended." + }, "locale": { "type": [ "string", @@ -5256,13 +6593,39 @@ "description": "Session locale (BCP 47, e.g. `uk-UA`).", "example": "uk-UA" }, - "model_id": { + "max_iterations": { + "type": [ + "integer", + "null" + ], + "description": "Maximum number of LLM iterations per turn for this session.", + "minimum": 0 + }, + "model_id": { + "type": [ + "string", + "null" + ], + "description": "The ID of the LLM model to use for this session.\nOverrides the agent's default model if specified.", + "example": "model_01933b5a00007000800000000000001" + }, + "network_access": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/NetworkAccessList", + "description": "Network access list controlling which hosts/URLs this session can reach.\nMerged with harness and agent layers (allowed: intersect, blocked: union)." + } + ] + }, + "system_prompt": { "type": [ "string", "null" ], - "description": "The ID of the LLM model to use for this session.\nOverrides the agent's default model if specified.", - "example": "model_01933b5a00007000800000000000001" + "description": "Optional session-level system prompt override.\nPrepended to the agent's system prompt when building RuntimeAgent." }, "tags": { "type": "array", @@ -5367,6 +6730,26 @@ } } }, + "DiffQuery": { + "type": "object", + "description": "Query for diff endpoint", + "required": [ + "oid" + ], + "properties": { + "base": { + "type": [ + "string", + "null" + ], + "description": "Base commit OID (optional, defaults to parent)" + }, + "oid": { + "type": "string", + "description": "Commit OID to diff (required)" + } + } + }, "ErrorResponse": { "type": "object", "description": "Standard error response for API endpoints.", @@ -5534,6 +6917,12 @@ { "$ref": "#/components/schemas/ToolCompletedData" }, + { + "$ref": "#/components/schemas/ToolProgressData" + }, + { + "$ref": "#/components/schemas/ToolOutputDeltaData" + }, { "$ref": "#/components/schemas/ToolCallRequestedData" }, @@ -5572,6 +6961,27 @@ }, { "$ref": "#/components/schemas/SubagentEventData" + }, + { + "$ref": "#/components/schemas/ContextCompactingData" + }, + { + "$ref": "#/components/schemas/ContextCompactedData" + }, + { + "$ref": "#/components/schemas/FileWrittenData" + }, + { + "$ref": "#/components/schemas/BudgetEventData" + }, + { + "$ref": "#/components/schemas/BudgetEventData" + }, + { + "$ref": "#/components/schemas/BudgetEventData" + }, + { + "$ref": "#/components/schemas/BudgetEventData" } ], "title": "EventData", @@ -5718,6 +7128,47 @@ } } }, + "FileWrittenData": { + "type": "object", + "description": "Data for file.written events emitted when files are written to the session filesystem.", + "required": [ + "path", + "operation", + "size_bytes", + "created" + ], + "properties": { + "created": { + "type": "boolean", + "description": "Whether this is a new file (true) or an update to an existing file (false)." + }, + "operation": { + "type": "string", + "description": "Operation type (see `FILE_OP_*` constants)." + }, + "path": { + "type": "string", + "description": "File path within the session filesystem (normalized, e.g. \"/reports/summary.md\")." + }, + "size_bytes": { + "type": "integer", + "format": "int64", + "description": "File size in bytes after write." + } + } + }, + "GetOrCreateChatSessionRequest": { + "type": "object", + "properties": { + "locale": { + "type": [ + "string", + "null" + ], + "description": "Browser locale for seeding the global chat session (BCP 47, e.g. `uk-UA`)." + } + } + }, "GetQuery": { "type": "object", "description": "Query parameters for GET requests", @@ -5739,6 +7190,132 @@ ], "description": "Unified response for GET that can be file or directory listing" }, + "GitCommitInfo": { + "type": "object", + "description": "A commit entry in the log", + "required": [ + "oid", + "message", + "author_name", + "author_email", + "timestamp", + "parent_oids" + ], + "properties": { + "author_email": { + "type": "string" + }, + "author_name": { + "type": "string" + }, + "message": { + "type": "string" + }, + "oid": { + "type": "string" + }, + "parent_oids": { + "type": "array", + "items": { + "type": "string" + } + }, + "timestamp": { + "type": "string", + "format": "date-time" + } + } + }, + "GitDiff": { + "type": "object", + "description": "A diff with full patch output", + "required": [ + "entries", + "stats" + ], + "properties": { + "entries": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GitDiffEntry" + } + }, + "patch": { + "type": [ + "string", + "null" + ] + }, + "stats": { + "$ref": "#/components/schemas/GitDiffStats" + } + } + }, + "GitDiffEntry": { + "type": "object", + "description": "A diff entry between two commits", + "required": [ + "path", + "status" + ], + "properties": { + "old_path": { + "type": [ + "string", + "null" + ] + }, + "path": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "GitDiffStats": { + "type": "object", + "description": "Diff statistics", + "required": [ + "files_changed", + "insertions", + "deletions" + ], + "properties": { + "deletions": { + "type": "integer", + "minimum": 0 + }, + "files_changed": { + "type": "integer", + "minimum": 0 + }, + "insertions": { + "type": "integer", + "minimum": 0 + } + } + }, + "GitRefInfo": { + "type": "object", + "description": "A git ref (branch pointer)", + "required": [ + "name", + "target", + "is_symbolic" + ], + "properties": { + "is_symbolic": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "target": { + "type": "string" + } + } + }, "GrepMatch": { "type": "object", "description": "Grep match result", @@ -5751,54 +7328,205 @@ "line": { "type": "string" }, - "line_number": { - "type": "integer", - "minimum": 0 + "line_number": { + "type": "integer", + "minimum": 0 + }, + "path": { + "type": "string" + } + } + }, + "GrepRequest": { + "type": "object", + "description": "Request to search files", + "required": [ + "pattern" + ], + "properties": { + "path_pattern": { + "type": [ + "string", + "null" + ], + "description": "Optional path pattern to filter files" + }, + "pattern": { + "type": "string", + "description": "Regex pattern to search for" + } + } + }, + "GrepResult": { + "type": "object", + "description": "Grep result for a file", + "required": [ + "path", + "matches" + ], + "properties": { + "matches": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GrepMatch" + } + }, + "path": { + "type": "string" + } + } + }, + "Harness": { + "type": "object", + "description": "Harness configuration for sessions.\nA harness defines the base behavior and capabilities that apply to all sessions.", + "required": [ + "id", + "name", + "system_prompt", + "status", + "created_at", + "updated_at" + ], + "properties": { + "archived_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Timestamp when the harness was archived." + }, + "capabilities": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AgentCapabilityConfig" + }, + "description": "Capabilities enabled for this harness with per-harness configuration." + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the harness was created." + }, + "default_model_id": { + "type": [ + "string", + "null" + ], + "description": "Default LLM model ID for this harness.\nLowest priority in chain: controls > session > agent > harness.", + "example": "model_01933b5a00007000800000000000001" + }, + "deleted_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Timestamp when the harness was deleted." + }, + "description": { + "type": [ + "string", + "null" + ], + "description": "Human-readable description of what the harness does." + }, + "display_name": { + "type": [ + "string", + "null" + ], + "description": "Human-readable display name shown in UI." + }, + "id": { + "type": "string", + "description": "Unique identifier for the harness (format: harness_{32-hex}).", + "example": "harness_01933b5a00007000800000000000001" + }, + "initial_files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InitialFile" + }, + "description": "Starter files copied into each new session for this harness." }, - "path": { - "type": "string" - } - } - }, - "GrepRequest": { - "type": "object", - "description": "Request to search files", - "required": [ - "pattern" - ], - "properties": { - "path_pattern": { + "is_built_in": { + "type": "boolean", + "description": "Whether this harness is built-in (system-managed, readonly).\nBuilt-in harnesses are provisioned during org initialization and\ncannot be modified or deleted via the API. Users can copy them." + }, + "name": { + "type": "string", + "description": "Name, unique per org (e.g. \"generic\")." + }, + "network_access": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/NetworkAccessList", + "description": "Network access list controlling which hosts/URLs sessions can reach.\nMerged with agent and session layers (allowed: intersect, blocked: union)." + } + ] + }, + "parent_harness_id": { "type": [ "string", "null" ], - "description": "Optional path pattern to filter files" + "description": "Optional parent harness that this harness inherits from.", + "example": "harness_01933b5a000070008000000000000602" }, - "pattern": { + "status": { + "$ref": "#/components/schemas/HarnessStatus", + "description": "Current lifecycle status of the harness." + }, + "system_prompt": { "type": "string", - "description": "Regex pattern to search for" + "description": "System prompt that defines the harness's base behavior.\nForms the foundation of the prompt stack." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Tags for organizing and filtering harnesses." + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the harness was last updated." } } }, - "GrepResult": { + "HarnessPreviewResponse": { "type": "object", - "description": "Grep result for a file", + "description": "Preview response showing merged prompt and tools", "required": [ - "path", - "matches" + "system_prompt", + "tools" ], "properties": { - "matches": { + "system_prompt": { + "type": "string" + }, + "tools": { "type": "array", "items": { - "$ref": "#/components/schemas/GrepMatch" + "type": "object" } - }, - "path": { - "type": "string" } } }, + "HarnessStatus": { + "type": "string", + "description": "Harness lifecycle status.\n- `active`: Harness is available for use\n- `archived`: Harness is hidden from listings and cannot be modified or assigned\n- `deleted`: Harness is a tombstone kept only for historical references", + "enum": [ + "active", + "archived", + "deleted" + ] + }, "ImageContentPart": { "type": "object", "description": "Image content part (base64 or URL)", @@ -6249,6 +7977,14 @@ "updated_at" ], "properties": { + "archived_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Timestamp when the agent was archived." + }, "capabilities": { "type": "array", "items": { @@ -6269,6 +8005,14 @@ "description": "Default LLM model ID for this agent.\nCan be overridden at the session level.", "example": "model_01933b5a00007000800000000000001" }, + "deleted_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Timestamp when the agent was deleted." + }, "description": { "type": [ "string", @@ -6276,6 +8020,13 @@ ], "description": "Human-readable description of what the agent does." }, + "display_name": { + "type": [ + "string", + "null" + ], + "description": "Human-readable display name shown in UI (e.g. \"Customer Support Agent\").\nFalls back to `name` when absent." + }, "id": { "type": "string", "description": "External identifier (agent_<32-hex>). Shown as \"id\" in API.\nClient-supplied or auto-generated.", @@ -6288,9 +8039,28 @@ }, "description": "Starter files copied into each new session for this agent." }, + "max_iterations": { + "type": [ + "integer", + "null" + ], + "description": "Maximum number of LLM iterations per turn for this agent.", + "minimum": 0 + }, "name": { "type": "string", - "description": "Display name of the agent." + "description": "Name, unique per org (e.g. \"customer-support\")." + }, + "network_access": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/NetworkAccessList", + "description": "Network access list controlling which hosts/URLs agent sessions can reach.\nMerged with harness and session layers (allowed: intersect, blocked: union)." + } + ] }, "status": { "$ref": "#/components/schemas/AgentStatus", @@ -6643,6 +8413,142 @@ } } }, + "ListResponse_Harness": { + "type": "object", + "description": "Response wrapper for list endpoints.\nAll list endpoints return responses wrapped in a `data` field.", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "description": "Harness configuration for sessions.\nA harness defines the base behavior and capabilities that apply to all sessions.", + "required": [ + "id", + "name", + "system_prompt", + "status", + "created_at", + "updated_at" + ], + "properties": { + "archived_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Timestamp when the harness was archived." + }, + "capabilities": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AgentCapabilityConfig" + }, + "description": "Capabilities enabled for this harness with per-harness configuration." + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the harness was created." + }, + "default_model_id": { + "type": [ + "string", + "null" + ], + "description": "Default LLM model ID for this harness.\nLowest priority in chain: controls > session > agent > harness.", + "example": "model_01933b5a00007000800000000000001" + }, + "deleted_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Timestamp when the harness was deleted." + }, + "description": { + "type": [ + "string", + "null" + ], + "description": "Human-readable description of what the harness does." + }, + "display_name": { + "type": [ + "string", + "null" + ], + "description": "Human-readable display name shown in UI." + }, + "id": { + "type": "string", + "description": "Unique identifier for the harness (format: harness_{32-hex}).", + "example": "harness_01933b5a00007000800000000000001" + }, + "initial_files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InitialFile" + }, + "description": "Starter files copied into each new session for this harness." + }, + "is_built_in": { + "type": "boolean", + "description": "Whether this harness is built-in (system-managed, readonly).\nBuilt-in harnesses are provisioned during org initialization and\ncannot be modified or deleted via the API. Users can copy them." + }, + "name": { + "type": "string", + "description": "Name, unique per org (e.g. \"generic\")." + }, + "network_access": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/NetworkAccessList", + "description": "Network access list controlling which hosts/URLs sessions can reach.\nMerged with agent and session layers (allowed: intersect, blocked: union)." + } + ] + }, + "parent_harness_id": { + "type": [ + "string", + "null" + ], + "description": "Optional parent harness that this harness inherits from.", + "example": "harness_01933b5a000070008000000000000602" + }, + "status": { + "$ref": "#/components/schemas/HarnessStatus", + "description": "Current lifecycle status of the harness." + }, + "system_prompt": { + "type": "string", + "description": "System prompt that defines the harness's base behavior.\nForms the foundation of the prompt stack." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Tags for organizing and filtering harnesses." + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the harness was last updated." + } + } + }, + "description": "Array of items returned by the list operation." + } + } + }, "ListResponse_KeyValueInfo": { "type": "object", "description": "Response wrapper for list endpoints.\nAll list endpoints return responses wrapped in a `data` field.", @@ -6945,11 +8851,31 @@ "type": "boolean", "description": "Whether an API key has been configured." }, + "archived_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Timestamp when the MCP server was archived." + }, + "auth_mode": { + "$ref": "#/components/schemas/McpServerAuthMode", + "description": "Authentication mode for this MCP server." + }, "created_at": { "type": "string", "format": "date-time", "description": "Timestamp when the MCP server was created." }, + "deleted_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Timestamp when the MCP server was deleted." + }, "description": { "type": [ "string", @@ -6978,6 +8904,13 @@ "description": "Display name of the MCP server.", "example": "atlassian-mcp-server" }, + "oauth_provider_id": { + "type": [ + "string", + "null" + ], + "description": "Stable provider id used for user-scoped OAuth connections." + }, "status": { "$ref": "#/components/schemas/McpServerStatus", "description": "Current lifecycle status of the MCP server." @@ -7109,11 +9042,32 @@ "updated_at" ], "properties": { + "base_harness_id": { + "type": [ + "string", + "null" + ], + "description": "Base harness used when session creation omits harness_id." + }, "created_at": { "type": "string", "format": "date-time", "description": "When the organization was created" }, + "default_harness_id": { + "type": [ + "string", + "null" + ], + "description": "Default harness to preselect in the UI." + }, + "default_model_id": { + "type": [ + "string", + "null" + ], + "description": "Default LLM model for the organization." + }, "id": { "type": "string", "description": "External identifier (org_<32-hex-chars>)" @@ -7198,6 +9152,13 @@ "null" ] }, + "archived_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, "compatibility": { "type": [ "string", @@ -7208,10 +9169,21 @@ "type": "string", "format": "date-time" }, + "deleted_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, "description": { "type": "string", "example": "Extract text and tables from PDF files." }, + "disable_model_invocation": { + "type": "boolean", + "description": "Whether the model is prevented from auto-invoking this skill" + }, "id": { "type": "string", "example": "skill_01933b5a00007000800000000000001" @@ -7633,6 +9605,13 @@ "format": "double", "description": "Cached read cost per million tokens (USD), if supported" }, + "cost_tiers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CostTier" + }, + "description": "Tiered pricing that applies above certain context thresholds.\nWhen present, the base cost fields apply up to the tier threshold,\nand each tier's costs apply for tokens beyond that threshold." + }, "input": { "type": "number", "format": "double", @@ -7666,6 +9645,14 @@ "format": "int32", "description": "Maximum input tokens (if different from context - output)" }, + "max_media": { + "type": [ + "integer", + "null" + ], + "format": "int32", + "description": "Maximum images or PDF pages per request" + }, "output": { "type": "integer", "format": "int32", @@ -7726,6 +9713,13 @@ } ] }, + "description": { + "type": [ + "string", + "null" + ], + "description": "Short human-readable description of the model's strengths and intended use" + }, "family": { "type": "string", "description": "Model family (e.g., \"gpt-4o\", \"claude-3-5-sonnet\")" @@ -8010,6 +10004,27 @@ } } }, + "LogQuery": { + "type": "object", + "description": "Query for log endpoint", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "description": "Max commits to return (default: 50)", + "minimum": 0 + }, + "ref": { + "type": [ + "string", + "null" + ], + "description": "Ref to start from (default: HEAD)" + } + } + }, "McpServer": { "type": "object", "description": "MCP Server configuration.\nRepresents a remote MCP server that can provide tools and resources.", @@ -8028,11 +10043,31 @@ "type": "boolean", "description": "Whether an API key has been configured." }, + "archived_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Timestamp when the MCP server was archived." + }, + "auth_mode": { + "$ref": "#/components/schemas/McpServerAuthMode", + "description": "Authentication mode for this MCP server." + }, "created_at": { "type": "string", "format": "date-time", "description": "Timestamp when the MCP server was created." }, + "deleted_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Timestamp when the MCP server was deleted." + }, "description": { "type": [ "string", @@ -8061,6 +10096,13 @@ "description": "Display name of the MCP server.", "example": "atlassian-mcp-server" }, + "oauth_provider_id": { + "type": [ + "string", + "null" + ], + "description": "Stable provider id used for user-scoped OAuth connections." + }, "status": { "$ref": "#/components/schemas/McpServerStatus", "description": "Current lifecycle status of the MCP server." @@ -8081,12 +10123,23 @@ } } }, + "McpServerAuthMode": { + "type": "string", + "description": "MCP server authentication mode.", + "enum": [ + "none", + "api_key", + "o_auth" + ] + }, "McpServerStatus": { "type": "string", - "description": "MCP Server lifecycle status.\n- `active`: Server is available for use\n- `disabled`: Server is disabled and not used", + "description": "MCP Server lifecycle status.\n- `active`: Server is available for use\n- `disabled`: Server is disabled and not used\n- `archived`: Server is hidden from listings and cannot be modified or assigned\n- `deleted`: Server is a tombstone kept only for historical references", "enum": [ "active", - "disabled" + "disabled", + "archived", + "deleted" ] }, "McpServerTransportType": { @@ -8096,6 +10149,36 @@ "http" ] }, + "McpToolAnnotations": { + "type": "object", + "description": "MCP tool annotations as defined by the MCP specification.\nAll fields are optional booleans following the MCP convention.", + "properties": { + "destructiveHint": { + "type": [ + "boolean", + "null" + ] + }, + "idempotentHint": { + "type": [ + "boolean", + "null" + ] + }, + "openWorldHint": { + "type": [ + "boolean", + "null" + ] + }, + "readOnlyHint": { + "type": [ + "boolean", + "null" + ] + } + } + }, "Message": { "type": "object", "description": "A message in the conversation", @@ -8200,7 +10283,8 @@ "text", "image", "audio", - "video" + "video", + "pdf" ] }, "ModelMetadata": { @@ -8250,6 +10334,26 @@ } } }, + "NetworkAccessList": { + "type": "object", + "description": "Network access list controlling which hosts/URLs an agent session can reach.\n\n- `allowed`: if non-empty, only URLs matching these patterns are permitted.\n- `blocked`: URLs matching these patterns are always denied (takes precedence over allowed).\n\nPattern format:\n- `example.com` — exact domain match (any port, any path)\n- `*.example.com` — domain and all subdomains\n- `https://example.com/api/` — exact URL prefix (scheme + host + path)", + "properties": { + "allowed": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Allowed host patterns. If non-empty, only matching URLs are permitted.\nAn empty list means \"no restriction from this layer\" (inherit parent)." + }, + "blocked": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Blocked host patterns. Always denied, even if matched by `allowed`." + } + } + }, "OrganizationResponse": { "type": "object", "description": "Response for organization operations", @@ -8260,11 +10364,32 @@ "updated_at" ], "properties": { + "base_harness_id": { + "type": [ + "string", + "null" + ], + "description": "Base harness used when session creation omits harness_id." + }, "created_at": { "type": "string", "format": "date-time", "description": "When the organization was created" }, + "default_harness_id": { + "type": [ + "string", + "null" + ], + "description": "Default harness to preselect in the UI." + }, + "default_model_id": { + "type": [ + "string", + "null" + ], + "description": "Default LLM model for the organization." + }, "id": { "type": "string", "description": "External identifier (org_<32-hex-chars>)" @@ -8311,61 +10436,232 @@ "$ref": "#/components/schemas/TokenUsage", "description": "Token usage" } - ] - } - } - }, - "OutputMessageDeltaData": { - "type": "object", - "description": "Data for output.message.delta event\n\nIncremental text update during LLM generation. Events are batched (~100ms)\nto reduce volume while providing real-time feedback.", - "required": [ - "turn_id", - "delta", - "accumulated" - ], - "properties": { - "accumulated": { - "type": "string", - "description": "Accumulated text so far" - }, - "delta": { - "type": "string", - "description": "The new text chunk" + ] + } + } + }, + "OutputMessageDeltaData": { + "type": "object", + "description": "Data for output.message.delta event\n\nIncremental text update during LLM generation. Events are batched (~100ms)\nto reduce volume while providing real-time feedback.", + "required": [ + "turn_id", + "delta", + "accumulated" + ], + "properties": { + "accumulated": { + "type": "string", + "description": "Accumulated text so far" + }, + "delta": { + "type": "string", + "description": "The new text chunk" + }, + "turn_id": { + "type": "string", + "description": "Turn ID this delta belongs to", + "example": "turn_01933b5a00007000800000000000001" + } + } + }, + "OutputMessageStartedData": { + "type": "object", + "description": "Data for output.message.started event\n\nEmitted when the LLM starts generating a response. UI can show a\n\"thinking\" indicator until output.message.delta or output.message.completed events arrive.", + "required": [ + "turn_id" + ], + "properties": { + "iteration": { + "type": [ + "integer", + "null" + ], + "format": "int32", + "description": "Current iteration number within this turn (1-based).\nUseful for UI to show progress during multi-step tool-calling flows.", + "minimum": 0 + }, + "model": { + "type": [ + "string", + "null" + ], + "description": "Optional model name being used" + }, + "turn_id": { + "type": "string", + "description": "Turn ID this output belongs to", + "example": "turn_01933b5a00007000800000000000001" + } + } + }, + "PaginatedResponse_Agent": { + "type": "object", + "description": "Response wrapper for paginated list endpoints.\nIncludes pagination metadata along with the data array.", + "required": [ + "data", + "total", + "offset", + "limit" + ], + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "description": "Agent configuration for agentic loop.\nAn agent defines the behavior and capabilities of an AI assistant.", + "required": [ + "id", + "name", + "system_prompt", + "status", + "created_at", + "updated_at" + ], + "properties": { + "archived_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Timestamp when the agent was archived." + }, + "capabilities": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AgentCapabilityConfig" + }, + "description": "Capabilities enabled for this agent with per-agent configuration.\nCapabilities add tools and system prompt modifications." + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the agent was created." + }, + "default_model_id": { + "type": [ + "string", + "null" + ], + "description": "Default LLM model ID for this agent.\nCan be overridden at the session level.", + "example": "model_01933b5a00007000800000000000001" + }, + "deleted_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Timestamp when the agent was deleted." + }, + "description": { + "type": [ + "string", + "null" + ], + "description": "Human-readable description of what the agent does." + }, + "display_name": { + "type": [ + "string", + "null" + ], + "description": "Human-readable display name shown in UI (e.g. \"Customer Support Agent\").\nFalls back to `name` when absent." + }, + "id": { + "type": "string", + "description": "External identifier (agent_<32-hex>). Shown as \"id\" in API.\nClient-supplied or auto-generated.", + "example": "agent_01933b5a000070008000000000000001" + }, + "initial_files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InitialFile" + }, + "description": "Starter files copied into each new session for this agent." + }, + "max_iterations": { + "type": [ + "integer", + "null" + ], + "description": "Maximum number of LLM iterations per turn for this agent.", + "minimum": 0 + }, + "name": { + "type": "string", + "description": "Name, unique per org (e.g. \"customer-support\")." + }, + "network_access": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/NetworkAccessList", + "description": "Network access list controlling which hosts/URLs agent sessions can reach.\nMerged with harness and session layers (allowed: intersect, blocked: union)." + } + ] + }, + "status": { + "$ref": "#/components/schemas/AgentStatus", + "description": "Current lifecycle status of the agent." + }, + "system_prompt": { + "type": "string", + "description": "System prompt that defines the agent's behavior.\nSent as the first message in every conversation." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Tags for organizing and filtering agents." + }, + "tools": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ToolDefinition" + }, + "description": "Client-side tools registered for this agent.\nThese tools are executed by the client, not the server." + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the agent was last updated." + }, + "usage": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/TokenUsage", + "description": "Cumulative token usage across all sessions for this agent." + } + ] + } + } + }, + "description": "Array of items returned by the list operation." }, - "turn_id": { - "type": "string", - "description": "Turn ID this delta belongs to", - "example": "turn_01933b5a00007000800000000000001" - } - } - }, - "OutputMessageStartedData": { - "type": "object", - "description": "Data for output.message.started event\n\nEmitted when the LLM starts generating a response. UI can show a\n\"thinking\" indicator until output.message.delta or output.message.completed events arrive.", - "required": [ - "turn_id" - ], - "properties": { - "iteration": { - "type": [ - "integer", - "null" - ], + "limit": { + "type": "integer", "format": "int32", - "description": "Current iteration number within this turn (1-based).\nUseful for UI to show progress during multi-step tool-calling flows.", + "description": "Maximum number of items per page.", "minimum": 0 }, - "model": { - "type": [ - "string", - "null" - ], - "description": "Optional model name being used" + "offset": { + "type": "integer", + "format": "int32", + "description": "Current offset (starting position).", + "minimum": 0 }, - "turn_id": { - "type": "string", - "description": "Turn ID this output belongs to", - "example": "turn_01933b5a00007000800000000000001" + "total": { + "type": "integer", + "format": "int32", + "description": "Total number of items matching the query (across all pages).", + "minimum": 0 } } }, @@ -8410,6 +10706,24 @@ "description": "ID of the agent working in this session (format: agent_{32-hex}). Optional.", "example": "agent_01933b5a00007000800000000000001" }, + "agent_identity_id": { + "type": [ + "string", + "null" + ], + "description": "Optional resident agent identity for unattended/background execution.", + "example": "identity_01933b5a00007000800000000000001" + }, + "blueprint_config": { + "description": "Validated config passed by host at blueprint spawn time." + }, + "blueprint_id": { + "type": [ + "string", + "null" + ], + "description": "Blueprint ID. When set, reason_activity and act_activity build RuntimeAgent\nfrom the blueprint definition instead of from harness_id/agent_id." + }, "capabilities": { "type": "array", "items": { @@ -8442,11 +10756,29 @@ "description": "ID of the harness for this session (format: harness_{32-hex}).", "example": "harness_01933b5a00007000800000000000001" }, + "hints": { + "type": [ + "object", + "null" + ], + "description": "Session-level client hints — arbitrary key-value pairs declared by the\nclient at session creation time. These are defaults for every turn;\nper-message `controls.hints` override these key-by-key (shallow merge).\n\nExamples: `{\"setup_connection\": true, \"rich_media\": true}`", + "additionalProperties": {}, + "propertyNames": { + "type": "string" + } + }, "id": { "type": "string", "description": "Unique identifier for the session (format: session_{32-hex}).", "example": "session_01933b5a00007000800000000000001" }, + "initial_files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InitialFile" + }, + "description": "Session-level initial files (additive to agent initial_files).\nFiles with matching paths override agent/harness files; new paths are appended." + }, "is_pinned": { "type": [ "boolean", @@ -8461,6 +10793,14 @@ ], "description": "Locale for localized agent behavior and formatting (BCP 47, e.g. `uk-UA`)." }, + "max_iterations": { + "type": [ + "integer", + "null" + ], + "description": "Maximum number of LLM iterations per turn for this session.", + "minimum": 0 + }, "model_id": { "type": [ "string", @@ -8469,6 +10809,17 @@ "description": "LLM model ID to use for this session (format: model_{32-hex}).\nOverrides the agent's default model if set.", "example": "model_01933b5a00007000800000000000001" }, + "network_access": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/NetworkAccessList", + "description": "Network access list controlling which hosts/URLs this session can reach.\nMerged with harness and agent layers (allowed: intersect, blocked: union)." + } + ] + }, "organization_id": { "type": "string", "description": "Organization this session belongs to (format: org_{32-hex}).", @@ -8532,6 +10883,13 @@ ], "description": "Original task description given to this subagent." }, + "system_prompt": { + "type": [ + "string", + "null" + ], + "description": "Session-level system prompt override.\nPrepended to the agent's system prompt when building RuntimeAgent." + }, "tags": { "type": "array", "items": { @@ -8621,6 +10979,39 @@ "type": "string", "description": "The base system prompt (before capability additions)", "example": "You are a helpful customer support agent." + }, + "tools": { + "type": "array", + "items": { + "type": "object" + }, + "description": "Client-side tools to include in the preview." + } + } + }, + "PreviewHarnessRequest": { + "type": "object", + "description": "Request to preview harness shape with capabilities applied", + "required": [ + "system_prompt" + ], + "properties": { + "capabilities": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AgentCapabilityConfig" + } + }, + "parent_harness_id": { + "type": [ + "string", + "null" + ], + "example": "harness_01933b5a000070008000000000000602" + }, + "system_prompt": { + "type": "string", + "example": "You are a research assistant." } } }, @@ -8846,6 +11237,25 @@ } } }, + "ResourceConfigResponse": { + "type": "object", + "description": "Response type for per-resource config endpoints.\n\nEvery resource exposes `GET /v1/{resource}/config` returning this type.\nUI uses it to gate controls (create/edit/delete buttons, admin panels).", + "required": [ + "policies" + ], + "properties": { + "policies": { + "type": "object", + "description": "Map of policy ID → whether the caller satisfies it.", + "additionalProperties": { + "type": "boolean" + }, + "propertyNames": { + "type": "string" + } + } + } + }, "RiskLevel": { "type": "string", "description": "Risk classification for capabilities (TM-AGENT-005).\n\nUsed to enforce approval requirements when assigning capabilities.", @@ -9205,6 +11615,24 @@ "description": "ID of the agent working in this session (format: agent_{32-hex}). Optional.", "example": "agent_01933b5a00007000800000000000001" }, + "agent_identity_id": { + "type": [ + "string", + "null" + ], + "description": "Optional resident agent identity for unattended/background execution.", + "example": "identity_01933b5a00007000800000000000001" + }, + "blueprint_config": { + "description": "Validated config passed by host at blueprint spawn time." + }, + "blueprint_id": { + "type": [ + "string", + "null" + ], + "description": "Blueprint ID. When set, reason_activity and act_activity build RuntimeAgent\nfrom the blueprint definition instead of from harness_id/agent_id." + }, "capabilities": { "type": "array", "items": { @@ -9237,11 +11665,29 @@ "description": "ID of the harness for this session (format: harness_{32-hex}).", "example": "harness_01933b5a00007000800000000000001" }, + "hints": { + "type": [ + "object", + "null" + ], + "description": "Session-level client hints — arbitrary key-value pairs declared by the\nclient at session creation time. These are defaults for every turn;\nper-message `controls.hints` override these key-by-key (shallow merge).\n\nExamples: `{\"setup_connection\": true, \"rich_media\": true}`", + "additionalProperties": {}, + "propertyNames": { + "type": "string" + } + }, "id": { "type": "string", "description": "Unique identifier for the session (format: session_{32-hex}).", "example": "session_01933b5a00007000800000000000001" }, + "initial_files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InitialFile" + }, + "description": "Session-level initial files (additive to agent initial_files).\nFiles with matching paths override agent/harness files; new paths are appended." + }, "is_pinned": { "type": [ "boolean", @@ -9256,6 +11702,14 @@ ], "description": "Locale for localized agent behavior and formatting (BCP 47, e.g. `uk-UA`)." }, + "max_iterations": { + "type": [ + "integer", + "null" + ], + "description": "Maximum number of LLM iterations per turn for this session.", + "minimum": 0 + }, "model_id": { "type": [ "string", @@ -9264,6 +11718,17 @@ "description": "LLM model ID to use for this session (format: model_{32-hex}).\nOverrides the agent's default model if set.", "example": "model_01933b5a00007000800000000000001" }, + "network_access": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/NetworkAccessList", + "description": "Network access list controlling which hosts/URLs this session can reach.\nMerged with harness and agent layers (allowed: intersect, blocked: union)." + } + ] + }, "organization_id": { "type": "string", "description": "Organization this session belongs to (format: org_{32-hex}).", @@ -9327,6 +11792,13 @@ ], "description": "Original task description given to this subagent." }, + "system_prompt": { + "type": [ + "string", + "null" + ], + "description": "Session-level system prompt override.\nPrepended to the agent's system prompt when building RuntimeAgent." + }, "tags": { "type": "array", "items": { @@ -9512,12 +11984,13 @@ }, "SessionStatus": { "type": "string", - "description": "Session execution status.\n- `started`: Session just created, no turn executed yet\n- `active`: A turn is currently running\n- `idle`: Turn completed, session waiting for next input", + "description": "Session execution status.\n- `started`: Session just created, no turn executed yet\n- `active`: A turn is currently running\n- `idle`: Turn completed, session waiting for next input\n- `paused`: Budget limit reached, waiting for user to increase limit or resume", "enum": [ "started", "active", "idle", - "waitingfortoolresults" + "waitingfortoolresults", + "paused" ] }, "Skill": { @@ -9538,7 +12011,14 @@ "type": [ "string", "null" - ] + ] + }, + "archived_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" }, "compatibility": { "type": [ @@ -9550,10 +12030,21 @@ "type": "string", "format": "date-time" }, + "deleted_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, "description": { "type": "string", "example": "Extract text and tables from PDF files." }, + "disable_model_invocation": { + "type": "boolean", + "description": "Whether the model is prevented from auto-invoking this skill" + }, "id": { "type": "string", "example": "skill_01933b5a00007000800000000000001" @@ -9642,7 +12133,9 @@ "description": "Skill lifecycle status", "enum": [ "active", - "disabled" + "disabled", + "archived", + "deleted" ] }, "SkillValidationResult": { @@ -9783,6 +12276,18 @@ } } }, + "SuccessResponse": { + "type": "object", + "description": "Generic success response", + "required": [ + "ok" + ], + "properties": { + "ok": { + "type": "boolean" + } + } + }, "SwitchOrgRequest": { "type": "object", "description": "Request to switch organization", @@ -10165,6 +12670,96 @@ } } }, + "ToolHints": { + "type": "object", + "description": "Semantic hints describing a tool's behavioral properties.\n\nFollows the MCP tool annotations convention (readOnlyHint, destructiveHint,\nidempotentHint, openWorldHint) plus everruns-specific hints. All fields are\noptional booleans — `None` means \"unknown/unspecified\". Consumers should\ntreat `None` as the conservative default (e.g., assume not readonly, assume\nnot idempotent).\n\nThese hints are informational — they do not enforce policy. Use `ToolPolicy`\nfor execution gating (auto vs requires_approval).", + "properties": { + "destructive": { + "type": [ + "boolean", + "null" + ], + "description": "Tool may irreversibly destroy or delete data.\nSubset of non-readonly — a tool can be non-readonly (writes) without\nbeing destructive (e.g., create/update operations)." + }, + "idempotent": { + "type": [ + "boolean", + "null" + ], + "description": "Calling the tool repeatedly with the same arguments produces the same\neffect. Safe to retry on transient failures." + }, + "long_running": { + "type": [ + "boolean", + "null" + ], + "description": "Tool may take significant time to complete (> ~5s typical).\nUseful for clients to show progress indicators and set timeouts." + }, + "narration_noun": { + "type": [ + "string", + "null" + ], + "description": "Entity noun for operation-based narration (e.g. \"agent\", \"harness\").\nWhen set, the narration system reads the `operation` argument and\nproduces verb-based narration like \"Created agent: Neon Cartographer\"\ninstead of the generic \"Ran Manage Agents\"." + }, + "open_world": { + "type": [ + "boolean", + "null" + ], + "description": "Tool interacts with external entities beyond the local system\n(network calls, third-party APIs, cloud services)." + }, + "persist_output": { + "type": [ + "boolean", + "null" + ], + "description": "Tool output should be persisted to session VFS before truncation.\nWhen set, the `tool_output_persistence` capability (EVE-222, EVE-245) writes\nstdout to `/.outputs/{tool_call_id}.stdout` and stderr to\n`/.outputs/{tool_call_id}.stderr`, injecting `full_output`, `total_lines`,\nand `output_files` into the result." + }, + "readonly": { + "type": [ + "boolean", + "null" + ], + "description": "Tool does not modify any state (read-only queries, lookups).\nWhen true: safe to call speculatively, result can be cached." + }, + "requires_secrets": { + "type": [ + "boolean", + "null" + ], + "description": "Tool requires API keys, credentials, or other secrets to function.\nUseful for UI to show connection prompts and for LLMs to anticipate\nauthentication failures." + } + } + }, + "ToolOutputDeltaData": { + "type": "object", + "description": "Data for tool.output.delta event.\n\nEmitted by tools during execution to stream incremental output chunks.\nThis enables live output rendering (e.g., bash stdout/stderr, command output)\nbetween tool.started and tool.completed. Generic — usable by any tool that\nproduces streamed output (bashkit, Daytona exec, subagent speech, etc.).\n\nThe consumer accumulates deltas by tool_call_id for display. The final\ntool.completed result is authoritative — deltas are informational only.", + "required": [ + "tool_call_id", + "tool_name", + "delta", + "stream" + ], + "properties": { + "delta": { + "type": "string", + "description": "Incremental output chunk" + }, + "stream": { + "type": "string", + "description": "Output stream identifier (e.g., \"stdout\", \"stderr\")" + }, + "tool_call_id": { + "type": "string", + "description": "Tool call ID this output belongs to" + }, + "tool_name": { + "type": "string", + "description": "Tool name" + } + } + }, "ToolPolicy": { "type": "string", "description": "Tool policy determines how tool calls are handled", @@ -10174,6 +12769,36 @@ "client_side" ] }, + "ToolProgressData": { + "type": "object", + "description": "Data for tool.progress event.\n\nEmitted by tools during execution to report interim status updates.\nThis allows long-running tools (e.g., browser operations, sandbox setup)\nto stream progress feedback between tool.started and tool.completed.", + "required": [ + "tool_call_id", + "tool_name", + "message" + ], + "properties": { + "display_name": { + "type": [ + "string", + "null" + ], + "description": "Human-readable display name for UI rendering" + }, + "message": { + "type": "string", + "description": "Human-readable status message (e.g., \"Connecting to browser…\")" + }, + "tool_call_id": { + "type": "string", + "description": "Tool call ID this progress belongs to" + }, + "tool_name": { + "type": "string", + "description": "Tool name" + } + } + }, "ToolResultContentPart": { "type": "object", "description": "Tool result content part (result of tool execution)", @@ -10413,6 +13038,14 @@ "description": "A human-readable description of what the agent does.", "example": "Updated description for the agent" }, + "display_name": { + "type": [ + "string", + "null" + ], + "description": "Human-readable display name shown in UI.", + "example": "Updated Support Agent" + }, "initial_files": { "type": [ "array", @@ -10423,13 +13056,32 @@ }, "description": "Starter files copied into each new session for this agent." }, + "max_iterations": { + "type": [ + "integer", + "null" + ], + "description": "Maximum number of LLM iterations per turn for this agent.", + "minimum": 0 + }, "name": { "type": [ "string", "null" ], - "description": "The name of the agent. Used for display purposes.", - "example": "Updated Support Agent" + "description": "Name, unique per org. Lowercase alphanumeric and hyphens.", + "example": "updated-support" + }, + "network_access": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/NetworkAccessList", + "description": "Network access list controlling which hosts/URLs this agent's sessions can reach.\nSend `{}` (empty object) to clear restrictions. Omit to leave unchanged." + } + ] }, "status": { "oneOf": [ @@ -10502,6 +13154,100 @@ } } }, + "UpdateHarnessRequest": { + "type": "object", + "description": "Request to update a harness. Only provided fields will be updated.", + "properties": { + "capabilities": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/AgentCapabilityConfig" + } + }, + "default_model_id": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "display_name": { + "type": [ + "string", + "null" + ], + "description": "Human-readable display name.", + "example": "Updated Research Harness" + }, + "initial_files": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/InitialFile" + } + }, + "name": { + "type": [ + "string", + "null" + ], + "description": "Name, unique per org.", + "example": "updated-research" + }, + "network_access": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/NetworkAccessList", + "description": "Network access list. Send `{}` (empty object) to clear. Omit to leave unchanged." + } + ] + }, + "parent_harness_id": { + "type": [ + "string", + "null" + ] + }, + "status": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/HarnessStatus" + } + ] + }, + "system_prompt": { + "type": [ + "string", + "null" + ] + }, + "tags": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + } + }, "UpdateLlmModelRequest": { "type": "object", "description": "Request to update an LLM model. Only provided fields will be updated.", @@ -10627,6 +13373,17 @@ ], "description": "API key for authentication. Set to update." }, + "auth_mode": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/McpServerAuthMode", + "description": "Authentication mode." + } + ] + }, "description": { "type": [ "string", @@ -10692,6 +13449,38 @@ "type": "object", "description": "Request to update an organization", "properties": { + "base_harness_id": { + "type": [ + "string", + "null" + ], + "description": "Base harness to use when a session is started without an explicit harness_id.", + "example": "harness_01933b5a000070008000000000000601" + }, + "default_harness_id": { + "type": [ + "string", + "null" + ], + "description": "Default harness to preselect in the UI for new sessions.\nMutually exclusive with `default_harness_name`.", + "example": "harness_01933b5a000070008000000000000602" + }, + "default_harness_name": { + "type": [ + "string", + "null" + ], + "description": "Alternative to `default_harness_id` — looked up by stable name within the org.\nMutually exclusive with `default_harness_id`.", + "example": "generic" + }, + "default_model_id": { + "type": [ + "string", + "null" + ], + "description": "Default LLM model for this organization. Must be an installed model.", + "example": "model_01933b5a00007000800000000000001" + }, "name": { "type": [ "string", @@ -10779,6 +13568,14 @@ "type": "object", "description": "Request to update a session. Only provided fields will be updated.", "properties": { + "agent_identity_id": { + "type": [ + "string", + "null" + ], + "description": "Optional resident agent identity used for unattended/background execution.", + "example": "identity_01933b5a00007000800000000000001" + }, "locale": { "type": [ "string", @@ -10958,6 +13755,10 @@ "name": "session-storage", "description": "Session key-value storage endpoints" }, + { + "name": "harnesses", + "description": "Harness management endpoints" + }, { "name": "skills", "description": "Skills registry endpoints" diff --git a/python/everruns_sdk/__init__.py b/python/everruns_sdk/__init__.py index d230039..46dbdc6 100644 --- a/python/everruns_sdk/__init__.py +++ b/python/everruns_sdk/__init__.py @@ -5,7 +5,7 @@ Quick Start: >>> from everruns_sdk import Everruns >>> client = Everruns() # uses EVERRUNS_API_KEY - >>> agent = await client.agents.create("Assistant", "You are helpful.") + >>> agent = await client.agents.create("assistant", "You are helpful.") """ from everruns_sdk.auth import ApiKey @@ -45,6 +45,7 @@ extract_tool_calls, generate_agent_id, generate_harness_id, + validate_agent_name, validate_harness_name, ) @@ -83,6 +84,7 @@ "extract_tool_calls", "generate_agent_id", "generate_harness_id", + "validate_agent_name", "validate_harness_name", ] diff --git a/python/everruns_sdk/client.py b/python/everruns_sdk/client.py index b028af2..b809205 100644 --- a/python/everruns_sdk/client.py +++ b/python/everruns_sdk/client.py @@ -33,6 +33,7 @@ ResumeSessionResponse, Session, SessionFile, + validate_agent_name, validate_harness_name, ) from everruns_sdk.sse import EventStream, StreamOptions @@ -55,7 +56,7 @@ class Everruns: Example: >>> client = Everruns() - >>> agent = await client.agents.create("Assistant", "You are helpful.") + >>> agent = await client.agents.create("assistant", "You are helpful.") """ def __init__( @@ -244,6 +245,7 @@ async def create( name: str, system_prompt: str, *, + display_name: Optional[str] = None, description: Optional[str] = None, default_model_id: Optional[str] = None, tags: Optional[list[str]] = None, @@ -253,16 +255,24 @@ async def create( """Create a new agent with a server-assigned ID. Args: - name: Display name of the agent. + name: Addressable name, unique per org + (format: ``[a-z0-9]+(-[a-z0-9]+)*``, max 64 chars). system_prompt: System prompt defining agent behavior. + display_name: Human-readable display name shown in UI. + Falls back to ``name`` when absent. description: Human-readable description. default_model_id: Default LLM model ID. tags: Tags for organizing agents. capabilities: Capabilities to enable. initial_files: Starter files copied into each new session for this agent. + + Raises: + ValueError: If ``name`` fails validation. """ + validate_agent_name(name) req = CreateAgentRequest( name=name, + display_name=display_name, system_prompt=system_prompt, description=description, default_model_id=default_model_id, @@ -279,6 +289,7 @@ async def apply( name: str, system_prompt: str, *, + display_name: Optional[str] = None, description: Optional[str] = None, default_model_id: Optional[str] = None, tags: Optional[list[str]] = None, @@ -293,17 +304,69 @@ async def apply( Args: id: Agent ID (format: ``agent_<32-hex>``). Use :func:`~everruns_sdk.generate_agent_id` to create one. - name: Display name of the agent. + name: Addressable name, unique per org + (format: ``[a-z0-9]+(-[a-z0-9]+)*``, max 64 chars). system_prompt: System prompt defining agent behavior. + display_name: Human-readable display name shown in UI. description: Human-readable description. default_model_id: Default LLM model ID. tags: Tags for organizing agents. capabilities: Capabilities to enable. initial_files: Starter files copied into each new session for this agent. + + Raises: + ValueError: If ``name`` fails validation. """ + validate_agent_name(name) req = CreateAgentRequest( id=id, name=name, + display_name=display_name, + system_prompt=system_prompt, + description=description, + default_model_id=default_model_id, + tags=tags or [], + capabilities=capabilities or [], + initial_files=initial_files or [], + ) + resp = await self._client._post("/agents", req.model_dump(exclude_none=True)) + return Agent(**resp) + + async def apply_by_name( + self, + name: str, + system_prompt: str, + *, + display_name: Optional[str] = None, + description: Optional[str] = None, + default_model_id: Optional[str] = None, + tags: Optional[list[str]] = None, + capabilities: Optional[list[AgentCapabilityConfig]] = None, + initial_files: Optional[list[InitialFile]] = None, + ) -> Agent: + """Create or update an agent by name (upsert). + + If an agent with the given ``name`` exists in the org, it is updated. + If not, a new agent is created. + + Args: + name: Addressable name, unique per org + (format: ``[a-z0-9]+(-[a-z0-9]+)*``, max 64 chars). + system_prompt: System prompt defining agent behavior. + display_name: Human-readable display name shown in UI. + description: Human-readable description. + default_model_id: Default LLM model ID. + tags: Tags for organizing agents. + capabilities: Capabilities to enable. + initial_files: Starter files copied into each new session for this agent. + + Raises: + ValueError: If ``name`` fails validation. + """ + validate_agent_name(name) + req = CreateAgentRequest( + name=name, + display_name=display_name, system_prompt=system_prompt, description=description, default_model_id=default_model_id, diff --git a/python/everruns_sdk/models.py b/python/everruns_sdk/models.py index 8b8e33d..2f52eb0 100644 --- a/python/everruns_sdk/models.py +++ b/python/everruns_sdk/models.py @@ -19,28 +19,51 @@ def generate_harness_id() -> str: return f"harness_{secrets.token_hex(16)}" -# Harness name validation: lowercase alphanumeric segments separated by hyphens -_HARNESS_NAME_PATTERN = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$") -_HARNESS_NAME_MAX_LENGTH = 64 +# Addressable name validation: lowercase alphanumeric segments separated by hyphens. +# Shared by harness names and agent names. +_ADDRESSABLE_NAME_PATTERN = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$") +_ADDRESSABLE_NAME_MAX_LENGTH = 64 -def validate_harness_name(name: str) -> str: - """Validate a harness name and return it if valid. +def _validate_addressable_name(name: str, *, label: str) -> str: + """Validate an addressable name and return it if valid. - Harness names must match ``[a-z0-9]+(-[a-z0-9]+)*`` and be at most 64 characters. + Names must match ``[a-z0-9]+(-[a-z0-9]+)*`` and be at most 64 characters. Raises: ValueError: If the name is invalid. """ - if len(name) > _HARNESS_NAME_MAX_LENGTH: + if len(name) > _ADDRESSABLE_NAME_MAX_LENGTH: raise ValueError( - f"harness_name must be at most {_HARNESS_NAME_MAX_LENGTH} characters, got {len(name)}" + f"{label} must be at most {_ADDRESSABLE_NAME_MAX_LENGTH} characters, got {len(name)}" ) - if not _HARNESS_NAME_PATTERN.match(name): - raise ValueError(f"harness_name must match pattern [a-z0-9]+(-[a-z0-9]+)*, got {name!r}") + if not _ADDRESSABLE_NAME_PATTERN.match(name): + raise ValueError(f"{label} must match pattern [a-z0-9]+(-[a-z0-9]+)*, got {name!r}") return name +def validate_harness_name(name: str) -> str: + """Validate a harness name and return it if valid. + + Harness names must match ``[a-z0-9]+(-[a-z0-9]+)*`` and be at most 64 characters. + + Raises: + ValueError: If the name is invalid. + """ + return _validate_addressable_name(name, label="harness_name") + + +def validate_agent_name(name: str) -> str: + """Validate an agent name and return it if valid. + + Agent names must match ``[a-z0-9]+(-[a-z0-9]+)*`` and be at most 64 characters. + + Raises: + ValueError: If the name is invalid. + """ + return _validate_addressable_name(name, label="agent_name") + + class AgentCapabilityConfig(BaseModel): """Per-agent capability configuration.""" @@ -70,6 +93,7 @@ class Agent(BaseModel): id: str name: str + display_name: Optional[str] = None description: Optional[str] = None system_prompt: str default_model_id: Optional[str] = None @@ -88,7 +112,15 @@ class CreateAgentRequest(BaseModel): default=None, description="Client-supplied agent ID (format: agent_{32-hex}). Auto-generated if omitted.", ) - name: str + name: str = Field( + description=( + "Addressable name, unique per org. Format: [a-z0-9]+(-[a-z0-9]+)*, max 64 chars." + ), + ) + display_name: Optional[str] = Field( + default=None, + description="Human-readable display name shown in UI. Falls back to name when absent.", + ) system_prompt: str description: Optional[str] = None default_model_id: Optional[str] = None diff --git a/python/examples/basic.py b/python/examples/basic.py index 10b32ee..a3729d8 100644 --- a/python/examples/basic.py +++ b/python/examples/basic.py @@ -12,7 +12,7 @@ async def main(): try: # Create an agent with current_time capability agent = await client.agents.create( - name="Example Assistant", + name="example-assistant-py", system_prompt="You are a helpful assistant for examples.", capabilities=[AgentCapabilityConfig(ref="current_time")], ) diff --git a/python/examples/initial_files.py b/python/examples/initial_files.py index 41ca104..05c0e1d 100644 --- a/python/examples/initial_files.py +++ b/python/examples/initial_files.py @@ -14,7 +14,7 @@ async def main(): try: agent = await client.agents.create( - name="Initial Files Example", + name="initial-files-example-py", system_prompt=("You are a helpful assistant. Read the starter files before answering."), ) diff --git a/python/tests/test_client.py b/python/tests/test_client.py index 9f8aef0..21eab8c 100644 --- a/python/tests/test_client.py +++ b/python/tests/test_client.py @@ -171,7 +171,7 @@ def test_create_agent_request_with_initial_files(): from everruns_sdk.models import CreateAgentRequest req = CreateAgentRequest( - name="Starter Agent", + name="starter-agent", system_prompt="You keep files ready.", initial_files=[ InitialFile( @@ -430,6 +430,68 @@ def test_create_session_request_harness_name_and_id_both(): assert "harness_name" in data +def test_validate_agent_name_valid(): + """Test validate_agent_name accepts valid names.""" + from everruns_sdk import validate_agent_name + + validate_agent_name("customer-support") + validate_agent_name("my-agent-v2") + validate_agent_name("a1b2") + validate_agent_name("x") + + +def test_validate_agent_name_too_long(): + """Test validate_agent_name rejects names over 64 chars.""" + from everruns_sdk import validate_agent_name + + with pytest.raises(ValueError, match="at most 64 characters"): + validate_agent_name("a" * 65) + + +def test_validate_agent_name_invalid_pattern(): + """Test validate_agent_name rejects invalid patterns.""" + from everruns_sdk import validate_agent_name + + with pytest.raises(ValueError, match="must match pattern"): + validate_agent_name("UPPER") + with pytest.raises(ValueError, match="must match pattern"): + validate_agent_name("has_underscore") + with pytest.raises(ValueError, match="must match pattern"): + validate_agent_name("-leading-dash") + with pytest.raises(ValueError, match="must match pattern"): + validate_agent_name("trailing-dash-") + with pytest.raises(ValueError, match="must match pattern"): + validate_agent_name("double--dash") + with pytest.raises(ValueError, match="must match pattern"): + validate_agent_name("") + + +def test_create_agent_request_with_display_name(): + """Test CreateAgentRequest serialization with display_name.""" + from everruns_sdk.models import CreateAgentRequest + + req = CreateAgentRequest( + name="customer-support", + display_name="Customer Support Agent", + system_prompt="You are helpful.", + ) + data = req.model_dump(exclude_none=True) + assert data["name"] == "customer-support" + assert data["display_name"] == "Customer Support Agent" + + +def test_create_agent_request_without_display_name(): + """Test CreateAgentRequest omits display_name when not set.""" + from everruns_sdk.models import CreateAgentRequest + + req = CreateAgentRequest( + name="customer-support", + system_prompt="You are helpful.", + ) + data = req.model_dump(exclude_none=True) + assert "display_name" not in data + + def test_create_agent_request_with_id(): """Test CreateAgentRequest serialization with client-supplied ID.""" from everruns_sdk.models import CreateAgentRequest, generate_agent_id @@ -437,12 +499,12 @@ def test_create_agent_request_with_id(): agent_id = generate_agent_id() req = CreateAgentRequest( id=agent_id, - name="Test Agent", + name="test-agent", system_prompt="You are helpful.", ) data = req.model_dump(exclude_none=True) assert data["id"] == agent_id - assert data["name"] == "Test Agent" + assert data["name"] == "test-agent" def test_create_agent_request_without_id(): @@ -450,7 +512,7 @@ def test_create_agent_request_without_id(): from everruns_sdk.models import CreateAgentRequest req = CreateAgentRequest( - name="Test Agent", + name="test-agent", system_prompt="You are helpful.", ) data = req.model_dump(exclude_none=True) @@ -637,7 +699,7 @@ async def test_create_agent_with_initial_files(): 201, json={ "id": "agent_123", - "name": "Starter Agent", + "name": "starter-agent", "system_prompt": "You keep files ready.", "initial_files": [ { @@ -657,7 +719,7 @@ async def test_create_agent_with_initial_files(): client = Everruns(api_key="evr_test_key") try: agent = await client.agents.create( - "Starter Agent", + "starter-agent", "You keep files ready.", initial_files=[ InitialFile( diff --git a/rust/examples/basic.rs b/rust/examples/basic.rs index 351ba1f..5935f14 100644 --- a/rust/examples/basic.rs +++ b/rust/examples/basic.rs @@ -15,7 +15,7 @@ async fn main() -> Result<(), Error> { .agents() .create_with_options( CreateAgentRequest::new( - "Example Assistant", + "example-assistant-rs", "You are a helpful assistant for examples.", ) .capabilities(vec![AgentCapabilityConfig::new("current_time")]), diff --git a/rust/examples/initial_files.rs b/rust/examples/initial_files.rs index 4bcc399..965d141 100644 --- a/rust/examples/initial_files.rs +++ b/rust/examples/initial_files.rs @@ -12,7 +12,7 @@ async fn main() -> Result<(), Error> { let agent = client .agents() .create( - "Initial Files Example", + "initial-files-example-rs", "You are a helpful assistant. Read the starter files before answering.", ) .await?; diff --git a/rust/src/client.rs b/rust/src/client.rs index f3ba3b6..0d29a2e 100644 --- a/rust/src/client.rs +++ b/rust/src/client.rs @@ -327,14 +327,19 @@ impl<'a> AgentsClient<'a> { self.client.get(&format!("/agents/{}", id)).await } - /// Create a new agent with a server-assigned ID + /// Create a new agent with a server-assigned ID. + /// + /// `name` is the addressable slug (e.g. `"customer-support"`), validated + /// against `[a-z0-9]+(-[a-z0-9]+)*`, max 64 chars. pub async fn create(&self, name: &str, system_prompt: &str) -> Result { + validate_agent_name(name)?; let req = CreateAgentRequest::new(name, system_prompt); self.client.post("/agents", &req).await } /// Create an agent with full options pub async fn create_with_options(&self, req: CreateAgentRequest) -> Result { + validate_agent_name(&req.name)?; self.client.post("/agents", &req).await } @@ -345,18 +350,36 @@ impl<'a> AgentsClient<'a> { /// /// Use [`generate_agent_id`] to create a properly formatted ID. pub async fn apply(&self, id: &str, name: &str, system_prompt: &str) -> Result { + validate_agent_name(name)?; let req = CreateAgentRequest::new(name, system_prompt).id(id); self.client.post("/agents", &req).await } - /// Create or update an agent with full options (upsert). + /// Create or update an agent with full options (upsert by ID). /// /// The `id` parameter is set on the request, overriding any existing value. pub async fn apply_with_options(&self, id: &str, req: CreateAgentRequest) -> Result { + validate_agent_name(&req.name)?; let req = req.id(id); self.client.post("/agents", &req).await } + /// Create or update an agent by name (upsert). + /// + /// If an agent with the given `name` exists in the org, it is updated. + /// If not, a new agent is created with that name. + pub async fn apply_by_name(&self, name: &str, system_prompt: &str) -> Result { + validate_agent_name(name)?; + let req = CreateAgentRequest::new(name, system_prompt); + self.client.post("/agents", &req).await + } + + /// Create or update an agent by name with full options (upsert). + pub async fn apply_by_name_with_options(&self, req: CreateAgentRequest) -> Result { + validate_agent_name(&req.name)?; + self.client.post("/agents", &req).await + } + /// Copy an agent, creating a new agent with the same configuration pub async fn copy(&self, id: &str) -> Result { self.client diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 2466e05..1f8ad7d 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -14,7 +14,7 @@ //! //! // Create an agent //! let agent = client.agents().create( -//! "Assistant", +//! "assistant", //! "You are a helpful assistant." //! ).await?; //! diff --git a/rust/src/models.rs b/rust/src/models.rs index 504d24a..d013d80 100644 --- a/rust/src/models.rs +++ b/rust/src/models.rs @@ -67,7 +67,11 @@ pub struct CapabilityInfo { #[non_exhaustive] pub struct Agent { pub id: String, + /// Addressable name, unique per org (e.g. "customer-support"). pub name: String, + /// Human-readable display name shown in UI. Falls back to `name` when absent. + #[serde(default)] + pub display_name: Option, #[serde(default)] pub description: Option, pub system_prompt: String, @@ -99,7 +103,12 @@ pub struct CreateAgentRequest { /// If not provided, one is auto-generated by the server. #[serde(skip_serializing_if = "Option::is_none")] pub id: Option, + /// Addressable name, unique per org. + /// Format: `[a-z0-9]+(-[a-z0-9]+)*`, max 64 chars. pub name: String, + /// Human-readable display name shown in UI. Falls back to `name` when absent. + #[serde(skip_serializing_if = "Option::is_none")] + pub display_name: Option, pub system_prompt: String, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, @@ -119,6 +128,7 @@ impl CreateAgentRequest { Self { id: None, name: name.into(), + display_name: None, system_prompt: system_prompt.into(), description: None, default_model_id: None, @@ -134,6 +144,12 @@ impl CreateAgentRequest { self } + /// Set the human-readable display name + pub fn display_name(mut self, display_name: impl Into) -> Self { + self.display_name = Some(display_name.into()); + self + } + /// Set the description pub fn description(mut self, description: impl Into) -> Self { self.description = Some(description.into()); @@ -274,13 +290,14 @@ impl InitialFile { } } -/// Harness name validation pattern: lowercase alphanumeric segments separated by hyphens. -/// Max 64 characters. -pub fn validate_harness_name(name: &str) -> crate::error::Result<()> { +/// Shared validation for addressable names (harness names, agent names). +/// Pattern: `[a-z0-9]+(-[a-z0-9]+)*`, max 64 characters. +fn validate_addressable_name(name: &str, label: &str) -> crate::error::Result<()> { const MAX_LEN: usize = 64; if name.len() > MAX_LEN { return Err(crate::error::Error::Validation(format!( - "harness_name must be at most {} characters, got {}", + "{} must be at most {} characters, got {}", + label, MAX_LEN, name.len() ))); @@ -295,13 +312,25 @@ pub fn validate_harness_name(name: &str) -> crate::error::Result<()> { }); if !valid { return Err(crate::error::Error::Validation(format!( - "harness_name must match pattern [a-z0-9]+(-[a-z0-9]+)*, got {:?}", - name + "{} must match pattern [a-z0-9]+(-[a-z0-9]+)*, got {:?}", + label, name ))); } Ok(()) } +/// Validate a harness name. +/// Pattern: `[a-z0-9]+(-[a-z0-9]+)*`, max 64 characters. +pub fn validate_harness_name(name: &str) -> crate::error::Result<()> { + validate_addressable_name(name, "harness_name") +} + +/// Validate an agent name. +/// Pattern: `[a-z0-9]+(-[a-z0-9]+)*`, max 64 characters. +pub fn validate_agent_name(name: &str) -> crate::error::Result<()> { + validate_addressable_name(name, "agent_name") +} + /// Request to create a session #[derive(Debug, Clone, Serialize)] #[non_exhaustive] diff --git a/rust/tests/client_test.rs b/rust/tests/client_test.rs index 6c636cf..50717d8 100644 --- a/rust/tests/client_test.rs +++ b/rust/tests/client_test.rs @@ -118,7 +118,7 @@ async fn test_create_agent_with_initial_files() { Mock::given(method("POST")) .and(path("/v1/agents")) .and(body_json(serde_json::json!({ - "name": "Starter Agent", + "name": "starter-agent", "system_prompt": "You keep files ready.", "initial_files": [ { @@ -131,7 +131,7 @@ async fn test_create_agent_with_initial_files() { }))) .respond_with(ResponseTemplate::new(201).set_body_json(serde_json::json!({ "id": "agent_123", - "name": "Starter Agent", + "name": "starter-agent", "description": null, "system_prompt": "You keep files ready.", "default_model_id": null, @@ -153,7 +153,7 @@ async fn test_create_agent_with_initial_files() { let agent = client .agents() .create_with_options( - CreateAgentRequest::new("Starter Agent", "You keep files ready.").initial_files(vec![ + CreateAgentRequest::new("starter-agent", "You keep files ready.").initial_files(vec![ InitialFile::new("/workspace/README.md", "# starter\n") .encoding("text") .is_readonly(true), diff --git a/rust/tests/serialization_test.rs b/rust/tests/serialization_test.rs index 3eba4e4..2d8da54 100644 --- a/rust/tests/serialization_test.rs +++ b/rust/tests/serialization_test.rs @@ -5,7 +5,7 @@ use everruns_sdk::{ Agent, AgentCapabilityConfig, CapabilityInfo, CreateAgentRequest, CreateMessageRequest, CreateSessionRequest, Event, ExternalActor, InitialFile, ListResponse, Message, Session, - generate_agent_id, generate_harness_id, validate_harness_name, + generate_agent_id, generate_harness_id, validate_agent_name, validate_harness_name, }; /// Test that ListResponse can be serialized and deserialized (round-trip) @@ -577,6 +577,61 @@ fn test_create_session_request_without_harness_name() { assert!(raw.get("harness_name").is_none()); } +/// Test validate_agent_name accepts valid names +#[test] +fn test_validate_agent_name_valid() { + assert!(validate_agent_name("customer-support").is_ok()); + assert!(validate_agent_name("my-agent-v2").is_ok()); + assert!(validate_agent_name("a1b2").is_ok()); + assert!(validate_agent_name("x").is_ok()); +} + +/// Test validate_agent_name rejects names over 64 chars +#[test] +fn test_validate_agent_name_too_long() { + let long_name = "a".repeat(65); + let result = validate_agent_name(&long_name); + assert!(result.is_err()); + assert!( + result + .unwrap_err() + .to_string() + .contains("at most 64 characters") + ); +} + +/// Test validate_agent_name rejects invalid patterns +#[test] +fn test_validate_agent_name_invalid_patterns() { + assert!(validate_agent_name("UPPER").is_err()); + assert!(validate_agent_name("has_underscore").is_err()); + assert!(validate_agent_name("-leading-dash").is_err()); + assert!(validate_agent_name("trailing-dash-").is_err()); + assert!(validate_agent_name("double--dash").is_err()); + assert!(validate_agent_name("").is_err()); +} + +/// Test CreateAgentRequest serialization with display_name +#[test] +fn test_create_agent_request_with_display_name() { + let req = CreateAgentRequest::new("customer-support", "You are helpful.") + .display_name("Customer Support Agent"); + let serialized = serde_json::to_string(&req).expect("should serialize"); + let raw: serde_json::Value = serde_json::from_str(&serialized).unwrap(); + assert_eq!(raw["name"], "customer-support"); + assert_eq!(raw["display_name"], "Customer Support Agent"); +} + +/// Test CreateAgentRequest without display_name omits it +#[test] +fn test_create_agent_request_without_display_name() { + let req = CreateAgentRequest::new("customer-support", "You are helpful."); + let serialized = serde_json::to_string(&req).expect("should serialize"); + let raw: serde_json::Value = serde_json::from_str(&serialized).unwrap(); + assert_eq!(raw["name"], "customer-support"); + assert!(raw.get("display_name").is_none()); +} + /// Test that Event serialization preserves the "type" field name (not "event_type") #[test] fn test_event_type_field_rename() { diff --git a/specs/api-surface.md b/specs/api-surface.md index 48726d7..38a26cd 100644 --- a/specs/api-surface.md +++ b/specs/api-surface.md @@ -16,11 +16,24 @@ SDKs cover agents and sessions functionality. No durable execution endpoints. - `GET /v1/agents/{id}/export` - Export agent as Markdown - `POST /v1/agents/{id}/copy` - Copy an agent -#### Client-Supplied Agent IDs +#### Agent Names + +Agent `name` is a URL/CLI-friendly addressable slug, **unique per org**. +Must match `[a-z0-9]+(-[a-z0-9]+)*`, max 64 characters (same rules as harness names). +Client-side validated via `validate_agent_name()`. + +The optional `display_name` field provides a human-readable label for UI rendering +(e.g. `"Customer Support Agent"`). Falls back to `name` when absent. + +#### Upsert Semantics + +`POST /v1/agents` supports two upsert modes: + +- **By ID**: When `id` is provided (format: `agent_<32-hex>`), if an agent with that ID + exists it is updated, otherwise a new agent is created. All SDKs expose `apply()`. +- **By name**: When `name` matches an existing agent in the org, that agent is updated. + All SDKs expose `apply_by_name()` / `applyByName()`. -The `POST /v1/agents` endpoint accepts an optional `id` field in the request body. -When `id` is provided (format: `agent_<32-hex>`), the endpoint has upsert semantics: -if an agent with that ID exists it is updated, otherwise a new agent is created. When `id` is omitted, the server auto-generates one (plain create). Agent create/update payloads also support optional `initial_files` starter files that are copied into each new session for that agent. diff --git a/typescript/examples/basic.ts b/typescript/examples/basic.ts index 37d919d..fa3a99e 100644 --- a/typescript/examples/basic.ts +++ b/typescript/examples/basic.ts @@ -13,7 +13,7 @@ async function main() { // Create an agent with current_time capability const agent = await client.agents.create({ - name: "Assistant", + name: "example-assistant-ts", systemPrompt: "You are a helpful assistant.", capabilities: [{ ref: "current_time" }], }); diff --git a/typescript/examples/initial-files.ts b/typescript/examples/initial-files.ts index 30e8eeb..21669fe 100644 --- a/typescript/examples/initial-files.ts +++ b/typescript/examples/initial-files.ts @@ -11,7 +11,7 @@ async function main() { const client = Everruns.fromEnv(); const agent = await client.agents.create({ - name: "Initial Files Example", + name: "initial-files-example-ts", systemPrompt: "You are a helpful assistant. Read the starter files before answering.", }); diff --git a/typescript/src/client.ts b/typescript/src/client.ts index 45a3dc2..3935a35 100644 --- a/typescript/src/client.ts +++ b/typescript/src/client.ts @@ -28,6 +28,7 @@ import { StreamOptions, TopUpRequest, UpdateBudgetRequest, + validateAgentName, validateHarnessName, } from "./models.js"; import { @@ -187,6 +188,7 @@ class AgentsClient { /** Create a new agent with a server-assigned ID. */ async create(request: CreateAgentRequest): Promise { + validateAgentName(request.name); return this.client.fetch("/agents", { method: "POST", body: JSON.stringify(toAgentBody(request)), @@ -202,12 +204,27 @@ class AgentsClient { * Use {@link generateAgentId} to create a properly formatted ID. */ async apply(id: string, request: CreateAgentRequest): Promise { + validateAgentName(request.name); return this.client.fetch("/agents", { method: "POST", body: JSON.stringify({ ...toAgentBody(request), id }), }); } + /** + * Create or update an agent by name (upsert). + * + * If an agent with the given `name` exists in the org, it is updated. + * If not, a new agent is created with that name. + */ + async applyByName(request: CreateAgentRequest): Promise { + validateAgentName(request.name); + return this.client.fetch("/agents", { + method: "POST", + body: JSON.stringify(toAgentBody(request)), + }); + } + async get(agentId: string): Promise { return this.client.fetch(`/agents/${agentId}`); } @@ -753,6 +770,9 @@ function toAgentBody(request: CreateAgentRequest): Record { if (request.id) { body.id = request.id; } + if (request.displayName) { + body.display_name = request.displayName; + } if (request.capabilities?.length) { body.capabilities = request.capabilities; } diff --git a/typescript/src/index.ts b/typescript/src/index.ts index 35624f5..2bf69d9 100644 --- a/typescript/src/index.ts +++ b/typescript/src/index.ts @@ -10,7 +10,7 @@ * * // Create an agent * const agent = await client.agents.create({ - * name: "Assistant", + * name: "assistant", * systemPrompt: "You are a helpful assistant." * }); * diff --git a/typescript/src/models.ts b/typescript/src/models.ts index 3e4962b..d01fe91 100644 --- a/typescript/src/models.ts +++ b/typescript/src/models.ts @@ -30,7 +30,10 @@ export interface CapabilityInfo { export interface Agent { id: string; + /** Addressable name, unique per org (e.g. "customer-support"). */ name: string; + /** Human-readable display name shown in UI. Falls back to name when absent. */ + displayName?: string | null; systemPrompt: string; model?: string; capabilities?: AgentCapabilityConfig[]; @@ -42,7 +45,14 @@ export interface Agent { export interface CreateAgentRequest { /** Client-supplied agent ID (format: agent_{32-hex}). Auto-generated if omitted. */ id?: string; + /** + * Addressable name, unique per org. + * Format: [a-z0-9]+(-[a-z0-9]+)*, max 64 chars. + * When a name matches an existing agent, the endpoint has upsert semantics. + */ name: string; + /** Human-readable display name shown in UI. Falls back to name when absent. */ + displayName?: string; systemPrompt: string; model?: string; capabilities?: AgentCapabilityConfig[]; @@ -117,9 +127,22 @@ export interface CreateSessionRequest { initialFiles?: InitialFile[]; } -/** Harness name validation pattern: lowercase alphanumeric segments separated by hyphens */ -const HARNESS_NAME_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/; -const HARNESS_NAME_MAX_LENGTH = 64; +/** Addressable name validation pattern: lowercase alphanumeric segments separated by hyphens */ +const ADDRESSABLE_NAME_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/; +const ADDRESSABLE_NAME_MAX_LENGTH = 64; + +function validateAddressableName(name: string, label: string): void { + if (name.length > ADDRESSABLE_NAME_MAX_LENGTH) { + throw new Error( + `${label} must be at most ${ADDRESSABLE_NAME_MAX_LENGTH} characters, got ${name.length}`, + ); + } + if (!ADDRESSABLE_NAME_PATTERN.test(name)) { + throw new Error( + `${label} must match pattern [a-z0-9]+(-[a-z0-9]+)*, got "${name}"`, + ); + } +} /** * Validate a harness name. @@ -128,16 +151,17 @@ const HARNESS_NAME_MAX_LENGTH = 64; * @throws Error if the name is invalid */ export function validateHarnessName(name: string): void { - if (name.length > HARNESS_NAME_MAX_LENGTH) { - throw new Error( - `harness_name must be at most ${HARNESS_NAME_MAX_LENGTH} characters, got ${name.length}`, - ); - } - if (!HARNESS_NAME_PATTERN.test(name)) { - throw new Error( - `harness_name must match pattern [a-z0-9]+(-[a-z0-9]+)*, got "${name}"`, - ); - } + validateAddressableName(name, "harness_name"); +} + +/** + * Validate an agent name. + * + * @param name - The agent name to validate + * @throws Error if the name is invalid + */ +export function validateAgentName(name: string): void { + validateAddressableName(name, "agent_name"); } /** External actor identity for messages from external channels (Slack, Discord, etc.) */ diff --git a/typescript/tests/client.test.ts b/typescript/tests/client.test.ts index a85dde0..81c920f 100644 --- a/typescript/tests/client.test.ts +++ b/typescript/tests/client.test.ts @@ -4,6 +4,7 @@ import { Everruns } from "../src/client.js"; import { generateAgentId, generateHarnessId, + validateAgentName, validateHarnessName, extractToolCalls, toolResult, @@ -149,7 +150,7 @@ describe("Everruns", () => { status: 201, json: async () => ({ id: "agent_123", - name: "Starter Agent", + name: "starter-agent", system_prompt: "You keep files ready.", initial_files: [ { @@ -171,7 +172,7 @@ describe("Everruns", () => { }); await client.agents.create({ - name: "Starter Agent", + name: "starter-agent", systemPrompt: "You keep files ready.", initialFiles: [ { @@ -188,7 +189,7 @@ describe("Everruns", () => { expect.objectContaining({ method: "POST", body: JSON.stringify({ - name: "Starter Agent", + name: "starter-agent", system_prompt: "You keep files ready.", initial_files: [ { @@ -419,6 +420,58 @@ describe("CreateSessionRequest with harnessName", () => { }); }); +describe("validateAgentName", () => { + it("should accept valid names", () => { + expect(() => validateAgentName("customer-support")).not.toThrow(); + expect(() => validateAgentName("my-agent-v2")).not.toThrow(); + expect(() => validateAgentName("a1b2")).not.toThrow(); + expect(() => validateAgentName("x")).not.toThrow(); + }); + + it("should reject names over 64 characters", () => { + expect(() => validateAgentName("a".repeat(65))).toThrow( + "at most 64 characters", + ); + }); + + it("should reject invalid patterns", () => { + expect(() => validateAgentName("UPPER")).toThrow("must match pattern"); + expect(() => validateAgentName("has_underscore")).toThrow( + "must match pattern", + ); + expect(() => validateAgentName("-leading-dash")).toThrow( + "must match pattern", + ); + expect(() => validateAgentName("trailing-dash-")).toThrow( + "must match pattern", + ); + expect(() => validateAgentName("double--dash")).toThrow( + "must match pattern", + ); + expect(() => validateAgentName("")).toThrow("must match pattern"); + }); +}); + +describe("CreateAgentRequest with displayName", () => { + it("should include displayName in request interface", () => { + const request: CreateAgentRequest = { + name: "customer-support", + displayName: "Customer Support Agent", + systemPrompt: "You are helpful.", + }; + expect(request.name).toBe("customer-support"); + expect(request.displayName).toBe("Customer Support Agent"); + }); + + it("should work without displayName", () => { + const request: CreateAgentRequest = { + name: "customer-support", + systemPrompt: "You are helpful.", + }; + expect(request.displayName).toBeUndefined(); + }); +}); + describe("EventsClient URL building", () => { it("should expand exclude as repeated query keys for events list", () => { // Verify the URLSearchParams approach used by EventsClient.list()