diff --git a/agentex-ui/lib/task-utils.test.ts b/agentex-ui/lib/task-utils.test.ts index 16e02a41..a8a10578 100644 --- a/agentex-ui/lib/task-utils.test.ts +++ b/agentex-ui/lib/task-utils.test.ts @@ -5,69 +5,49 @@ import { createTaskName } from '@/lib/task-utils'; import type { TaskListResponse } from 'agentex/resources'; describe('createTaskName', () => { - it('returns description when params.description is a string', () => { + it('returns task_metadata.display_name when present', () => { const task = { id: '123', - params: { - description: 'Test task description', - }, - } as TaskListResponse.TaskListResponseItem; - - expect(createTaskName(task)).toBe('Test task description'); - }); - - it('returns "Unnamed task" when params.description is missing', () => { - const task = { - params: {}, - } as TaskListResponse.TaskListResponseItem; - - expect(createTaskName(task)).toBe('Unnamed task'); - }); - - it('returns "Unnamed task" when params is missing', () => { - const task = {} as TaskListResponse.TaskListResponseItem; + task_metadata: { display_name: 'My task' }, + } as unknown as TaskListResponse.TaskListResponseItem; - expect(createTaskName(task)).toBe('Unnamed task'); + expect(createTaskName(task)).toBe('My task'); }); - it('returns "Unnamed task" when params.description is not a string', () => { + it('strips the legacy scheduled-message prefix from display_name', () => { const task = { - params: { - description: 123, + id: '123', + task_metadata: { + display_name: 'Scheduled Message: Daily digest', + schedule_id: 'sched-1', }, } as unknown as TaskListResponse.TaskListResponseItem; - expect(createTaskName(task)).toBe('Unnamed task'); + expect(createTaskName(task)).toBe('Daily digest'); }); - it('returns "Unnamed task" when params.description is null', () => { + it('falls back to the task name when there is no display_name', () => { const task = { - params: { - description: null, - }, + id: '123', + name: 'my-task-name', } as unknown as TaskListResponse.TaskListResponseItem; - expect(createTaskName(task)).toBe('Unnamed task'); + expect(createTaskName(task)).toBe('my-task-name'); }); - it('returns "Unnamed task" when params.description is undefined', () => { + it('returns "Unnamed task" when neither display_name nor name is set', () => { const task = { id: '123', - params: { - description: undefined, - }, - } as TaskListResponse.TaskListResponseItem; + } as unknown as TaskListResponse.TaskListResponseItem; expect(createTaskName(task)).toBe('Unnamed task'); }); - it('returns "Unnamed task" for empty string description', () => { + it('returns "Unnamed task" when name is an empty string', () => { const task = { id: '123', - params: { - description: '', - }, - } as TaskListResponse.TaskListResponseItem; + name: '', + } as unknown as TaskListResponse.TaskListResponseItem; expect(createTaskName(task)).toBe('Unnamed task'); }); diff --git a/agentex-ui/lib/task-utils.ts b/agentex-ui/lib/task-utils.ts index 9a6d2e7c..1e5a3e23 100644 --- a/agentex-ui/lib/task-utils.ts +++ b/agentex-ui/lib/task-utils.ts @@ -20,11 +20,8 @@ export function createTaskName( return displayName; } - if ( - task?.params?.description && - typeof task.params.description === 'string' - ) { - return task.params.description; + if (typeof task?.name === 'string' && task.name) { + return task.name; } return 'Unnamed task'; diff --git a/agentex/openapi.yaml b/agentex/openapi.yaml index 904f1615..b8ab2b22 100644 --- a/agentex/openapi.yaml +++ b/agentex/openapi.yaml @@ -555,7 +555,8 @@ paths: tags: - Tasks summary: List Tasks - description: List all tasks. + description: List tasks. Returns a lean summary per task and omits `params`; + fetch GET /tasks/{task_id} for the full record including `params`. operationId: list_tasks_tasks_get parameters: - name: agent_id @@ -641,7 +642,7 @@ paths: schema: type: array items: - $ref: '#/components/schemas/TaskResponse' + $ref: '#/components/schemas/TaskSummary' title: Response List Tasks Tasks Get '422': description: Validation Error @@ -6845,6 +6846,68 @@ components: title: Optional reason for the status change type: object title: TaskStatusReasonRequest + TaskSummary: + properties: + id: + type: string + title: Unique Task ID + name: + anyOf: + - type: string + - type: 'null' + title: Unique name of the task + status: + anyOf: + - $ref: '#/components/schemas/TaskStatus' + - type: 'null' + title: The current status of the task + status_reason: + anyOf: + - type: string + - type: 'null' + title: The reason for the current task status + created_at: + anyOf: + - type: string + format: date-time + - type: 'null' + title: The timestamp when the task was created + updated_at: + anyOf: + - type: string + format: date-time + - type: 'null' + title: The timestamp when the task was last updated + cleaned_at: + anyOf: + - type: string + format: date-time + - type: 'null' + title: The timestamp when the task's content was cleaned for retention compliance; + null when active + task_metadata: + anyOf: + - additionalProperties: true + type: object + - type: 'null' + title: Task metadata + agents: + anyOf: + - items: + $ref: '#/components/schemas/Agent' + type: array + - type: 'null' + title: Agents associated with this task (only populated when 'agents' view + is requested) + type: object + required: + - id + title: TaskSummary + description: 'Lean list-response shape. Omits `params` (the arbitrary create-time + + payload, which can carry per-caller secrets and PII); fetch GET /tasks/{id} + + for the full record.' TextContent: properties: type: diff --git a/agentex/src/api/routes/tasks.py b/agentex/src/api/routes/tasks.py index 86abb22a..6c9dab91 100644 --- a/agentex/src/api/routes/tasks.py +++ b/agentex/src/api/routes/tasks.py @@ -17,6 +17,7 @@ TaskResponse, TaskStatus, TaskStatusReasonRequest, + TaskSummary, UpdateTaskRequest, ) from src.domain.entities.tasks import TaskStatus as DomainTaskStatus @@ -73,9 +74,12 @@ async def get_task_by_name( @router.get( "", - response_model=list[TaskResponse], + response_model=list[TaskSummary], summary="List Tasks", - description="List all tasks.", + description=( + "List tasks. Returns a lean summary per task and omits `params`; " + "fetch GET /tasks/{task_id} for the full record including `params`." + ), ) async def list_tasks( task_use_case: DTaskUseCase, @@ -143,7 +147,7 @@ async def list_tasks( order_direction=order_direction, relationships=relationships, ) - return [TaskResponse.model_validate(task_entity) for task_entity in task_entities] + return [TaskSummary.model_validate(task_entity) for task_entity in task_entities] @router.delete( diff --git a/agentex/src/api/schemas/tasks.py b/agentex/src/api/schemas/tasks.py index a79d9fef..f5efaad6 100644 --- a/agentex/src/api/schemas/tasks.py +++ b/agentex/src/api/schemas/tasks.py @@ -74,6 +74,49 @@ class TaskResponse(Task): ) +class TaskSummary(BaseModel): + """Lean list-response shape. Omits `params` (the arbitrary create-time + payload, which can carry per-caller secrets and PII); fetch GET /tasks/{id} + for the full record.""" + + id: str = Field( + ..., + title="Unique Task ID", + ) + name: str | None = Field( + None, + title="Unique name of the task", + ) + status: TaskStatus | None = Field( + None, + title="The current status of the task", + ) + status_reason: str | None = Field( + None, + title="The reason for the current task status", + ) + created_at: datetime | None = Field( + None, + title="The timestamp when the task was created", + ) + updated_at: datetime | None = Field( + None, + title="The timestamp when the task was last updated", + ) + cleaned_at: datetime | None = Field( + None, + title="The timestamp when the task's content was cleaned for retention compliance; null when active", + ) + task_metadata: dict[str, Any] | None = Field( + None, + title="Task metadata", + ) + agents: list["Agent"] | None = Field( + default=None, + title="Agents associated with this task (only populated when 'agents' view is requested)", + ) + + class UpdateTaskRequest(BaseModel): task_metadata: dict[str, Any] | None = Field( None, diff --git a/agentex/tests/integration/api/tasks/test_tasks_api.py b/agentex/tests/integration/api/tasks/test_tasks_api.py index bdf857bd..dbf0a1a4 100644 --- a/agentex/tests/integration/api/tasks/test_tasks_api.py +++ b/agentex/tests/integration/api/tasks/test_tasks_api.py @@ -141,6 +141,10 @@ async def test_list_tasks_returns_valid_structure_and_schema( for task in tasks: assert "id" in task and isinstance(task["id"], str) + # The list is a lean summary and must never carry `params` (the + # arbitrary create-time payload that can hold secrets/PII). + assert "params" not in task + # Check if this is our test task if task["id"] == test_task.id: found_test_task = True @@ -499,32 +503,24 @@ async def test_get_task_by_name_non_existent_returns_404(self, isolated_client): assert response.status_code == 404 # - async def test_list_tasks_includes_params_in_response( + async def test_list_tasks_omits_params_in_response( self, isolated_client, test_task_with_params ): - """Test that list tasks endpoint includes params field in response""" + """The list summary must omit `params` even when the task has them + (they can carry secrets/PII); fetch a single task for the full record.""" # When - Request all tasks response = await isolated_client.get("/tasks") - # Then - Should succeed and include params in response + # Then - The task is present but its params are not serialized assert response.status_code == 200 tasks = response.json() assert isinstance(tasks, list) - # Find our test task with params params_task = next( (task for task in tasks if task["id"] == test_task_with_params.id), None ) - assert params_task is not None, "Task with params should be in the list" - - # Verify params field exists and has correct structure - assert "params" in params_task - assert params_task["params"] is not None - assert params_task["params"]["model"] == "gpt-4" - assert params_task["params"]["temperature"] == 0.8 - assert params_task["params"]["max_tokens"] == 2000 - assert params_task["params"]["nested"]["setting"] == "value" - assert params_task["params"]["nested"]["numbers"] == [1, 2, 3] + assert params_task is not None, "Task should be in the list" + assert "params" not in params_task # async def test_get_task_by_id_includes_params_in_response( @@ -579,26 +575,22 @@ async def test_get_task_by_name_includes_params_in_response( assert task_data["params"]["nested"]["numbers"] == [1, 2, 3] # - async def test_list_tasks_handles_null_params_correctly( + async def test_list_tasks_omits_params_for_null_params_task( self, isolated_client, test_task ): - """Test that list tasks handles tasks with null params correctly""" + """A task created without params also has no `params` key in the list.""" # When - Request all tasks (test_task has null params) response = await isolated_client.get("/tasks") - # Then - Should succeed and handle null params + # Then - The task is present with no params field assert response.status_code == 200 tasks = response.json() - # Find our test task without params null_params_task = next( (task for task in tasks if task["id"] == test_task.id), None ) assert null_params_task is not None - - # Verify params field exists and is null - assert "params" in null_params_task - assert null_params_task["params"] is None + assert "params" not in null_params_task # async def test_get_task_by_id_handles_null_params_correctly(