Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 19 additions & 39 deletions agentex-ui/lib/task-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
Expand Down
7 changes: 2 additions & 5 deletions agentex-ui/lib/task-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
67 changes: 65 additions & 2 deletions agentex/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
10 changes: 7 additions & 3 deletions agentex/src/api/routes/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
TaskResponse,
TaskStatus,
TaskStatusReasonRequest,
TaskSummary,
UpdateTaskRequest,
)
from src.domain.entities.tasks import TaskStatus as DomainTaskStatus
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
43 changes: 43 additions & 0 deletions agentex/src/api/schemas/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
36 changes: 14 additions & 22 deletions agentex/tests/integration/api/tasks/test_tasks_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
Loading