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
37 changes: 26 additions & 11 deletions api/openapi/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -948,7 +948,8 @@ paths:
description: >-
Lists agents in every project of the organization where the caller can
read agents. Each agent carries its project_id. Items are ordered by
created_at descending, then id descending.
created_at descending, then id descending. Pass `activity_state` to keep
only agents in that activity.
parameters:
- $ref: '#/components/parameters/ResourceNameFilter'
- name: agent_profile_id
Expand All @@ -973,6 +974,7 @@ paths:
description: Include archived agents. Defaults to false.
schema:
type: boolean
- $ref: '#/components/parameters/AgentActivityStateFilter'
- $ref: '#/components/parameters/AgentIncludeUsage'
- name: sort
in: query
Expand Down Expand Up @@ -4528,7 +4530,8 @@ paths:
summary: List agents
description: >-
Lists the project's agents with their current activity, newest first by default. Archived
agents and subagents are excluded unless requested.
agents and subagents are excluded unless requested. Pass `activity_state` to keep only
agents in that activity.
parameters:
- $ref: '#/components/parameters/ResourceNameFilter'
- name: agent_profile_id
Expand All @@ -4553,6 +4556,7 @@ paths:
description: Include archived agents. Defaults to false.
schema:
type: boolean
- $ref: '#/components/parameters/AgentActivityStateFilter'
- $ref: '#/components/parameters/AgentIncludeUsage'
- name: sort
in: query
Expand Down Expand Up @@ -8910,6 +8914,15 @@ components:
bearerFormat: Omnara machine daemon token
description: An opaque Omnara machine-daemon bearer token. The bootstrap route derives the organization, installation, and machine identity from the credential; runtime routes verify that the addressed runtime belongs to the token-derived machine.
parameters:
AgentActivityStateFilter:
name: activity_state
in: query
required: false
schema:
$ref: '#/components/schemas/AgentActivityState'
description: >-
Return only agents whose current `activity.state` matches. `archived`
includes archived agents even when `include_archived` is omitted.
AgentIncludeUsage:
name: include_usage
in: query
Expand Down Expand Up @@ -12919,21 +12932,23 @@ components:
$ref: '#/components/schemas/Timestamp'
archived_at:
$ref: '#/components/schemas/Timestamp'
AgentActivityState:
description: >-
running while the agent has work in progress, which includes a
custom tool call awaiting its result; waiting_on_interaction while
it has an open question or permission request; idle otherwise; and
archived once the agent is archived. Running processes and working
subagents do not change it; see running_processes and
working_subagents.
type: string
enum: [running, idle, waiting_on_interaction, archived]
AgentActivity:
type: object
additionalProperties: false
required: [state, last_activity_at, open_interactions, running_processes, working_subagents]
properties:
state:
description: >-
running while the agent has work in progress, which includes a
custom tool call awaiting its result; waiting_on_interaction while
it has an open question or permission request; idle otherwise; and
archived once the agent is archived. Running processes and working
subagents do not change it; see running_processes and
working_subagents.
type: string
enum: [running, idle, waiting_on_interaction, archived]
$ref: '#/components/schemas/AgentActivityState'
last_activity_at:
description: When the agent's latest event was recorded, or its creation time before any event.
$ref: '#/components/schemas/Timestamp'
Expand Down
2 changes: 1 addition & 1 deletion docs/agents/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ reference has since been deleted.

## Find agents

The project's **Agents** page opens on **Profiles**; switch to the **Instances** tab to list active agents, and click one to open its conversation. `omnara agents list` and the API return active agents newest first; fetch one by ID with `omnara agents get {agent-id}`. Both responses include each agent's `activity`, whose `state` is `running`, `waiting_on_interaction`, `idle`, or `archived`. Alongside the state, `activity` always reports what else is open or in progress: `open_interactions` counts open questions and permission requests, `running_processes` lists commands the agent left running that will message it when they end, and `working_subagents` lists subagents that are still working. An agent can be `waiting_on_interaction` with a process running, or `idle` but about to wake when its build finishes. Archived agents are omitted from the list but remain retrievable by ID.
The project's **Agents** page opens on **Profiles**; switch to the **Instances** tab to list active agents, and click one to open its conversation. `omnara agents list` and the API return active agents newest first; fetch one by ID with `omnara agents get {agent-id}`. Both responses include each agent's `activity`, whose `state` is `running`, `waiting_on_interaction`, `idle`, or `archived`. Alongside the state, `activity` always reports what else is open or in progress: `open_interactions` counts open questions and permission requests, `running_processes` lists commands the agent left running that will message it when they end, and `working_subagents` lists subagents that are still working. An agent can be `waiting_on_interaction` with a process running, or `idle` but about to wake when its build finishes. Pass `activity_state` (`--activity-state` in the CLI) to keep only agents in that activity. `activity_state=archived` includes archived agents even when `include_archived` is omitted. The Instances filters menu offers the same status choices. Archived agents are otherwise omitted from the list but remain retrievable by ID.

<Info>
For full schemas and playground see: [List agents](/api-reference/endpoints/agents/list-agents) and [Get agent](/api-reference/endpoints/agents/get-agent).
Expand Down
37 changes: 26 additions & 11 deletions docs/api-reference/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -949,7 +949,8 @@ paths:
description: >-
Lists agents in every project of the organization where the caller can
read agents. Each agent carries its project_id. Items are ordered by
created_at descending, then id descending.
created_at descending, then id descending. Pass `activity_state` to keep
only agents in that activity.
parameters:
- $ref: '#/components/parameters/ResourceNameFilter'
- name: agent_profile_id
Expand All @@ -974,6 +975,7 @@ paths:
description: Include archived agents. Defaults to false.
schema:
type: boolean
- $ref: '#/components/parameters/AgentActivityStateFilter'
- $ref: '#/components/parameters/AgentIncludeUsage'
- name: sort
in: query
Expand Down Expand Up @@ -4529,7 +4531,8 @@ paths:
summary: List agents
description: >-
Lists the project's agents with their current activity, newest first by default. Archived
agents and subagents are excluded unless requested.
agents and subagents are excluded unless requested. Pass `activity_state` to keep only
agents in that activity.
parameters:
- $ref: '#/components/parameters/ResourceNameFilter'
- name: agent_profile_id
Expand All @@ -4554,6 +4557,7 @@ paths:
description: Include archived agents. Defaults to false.
schema:
type: boolean
- $ref: '#/components/parameters/AgentActivityStateFilter'
- $ref: '#/components/parameters/AgentIncludeUsage'
- name: sort
in: query
Expand Down Expand Up @@ -8911,6 +8915,15 @@ components:
bearerFormat: Omnara machine daemon token
description: An opaque Omnara machine-daemon bearer token. The bootstrap route derives the organization, installation, and machine identity from the credential; runtime routes verify that the addressed runtime belongs to the token-derived machine.
parameters:
AgentActivityStateFilter:
name: activity_state
in: query
required: false
schema:
$ref: '#/components/schemas/AgentActivityState'
description: >-
Return only agents whose current `activity.state` matches. `archived`
includes archived agents even when `include_archived` is omitted.
AgentIncludeUsage:
name: include_usage
in: query
Expand Down Expand Up @@ -12920,21 +12933,23 @@ components:
$ref: '#/components/schemas/Timestamp'
archived_at:
$ref: '#/components/schemas/Timestamp'
AgentActivityState:
description: >-
running while the agent has work in progress, which includes a
custom tool call awaiting its result; waiting_on_interaction while
it has an open question or permission request; idle otherwise; and
archived once the agent is archived. Running processes and working
subagents do not change it; see running_processes and
working_subagents.
type: string
enum: [running, idle, waiting_on_interaction, archived]
AgentActivity:
type: object
additionalProperties: false
required: [state, last_activity_at, open_interactions, running_processes, working_subagents]
properties:
state:
description: >-
running while the agent has work in progress, which includes a
custom tool call awaiting its result; waiting_on_interaction while
it has an open question or permission request; idle otherwise; and
archived once the agent is archived. Running processes and working
subagents do not change it; see running_processes and
working_subagents.
type: string
enum: [running, idle, waiting_on_interaction, archived]
$ref: '#/components/schemas/AgentActivityState'
last_activity_at:
description: When the agent's latest event was recorded, or its creation time before any event.
$ref: '#/components/schemas/Timestamp'
Expand Down
104 changes: 104 additions & 0 deletions frontend/apps/web/src/components/agents/AgentsSection.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/** @vitest-environment happy-dom */

import { OmnaraClientProvider } from '@omnara/react'
import { createOmnaraClient } from '@omnara/sdk'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { act } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterAll, afterEach, beforeAll, beforeEach, expect, it, vi } from 'vitest'

import { AgentsSection } from '@/components/agents/AgentsSection'
import { fakeApi, jsonResponse } from '@/test/fake-api'
import { fakeId } from '@/test/fixtures'
import { enableReactActEnvironment } from '@/test/react-act'

const orgId = fakeId('org')
const projectId = fakeId('proj')

let container: HTMLDivElement
let root: Root
let restoreActEnvironment: () => void

beforeAll(() => {
restoreActEnvironment = enableReactActEnvironment()
})

afterAll(() => {
restoreActEnvironment()
})

beforeEach(() => {
container = document.createElement('div')
document.body.append(container)
root = createRoot(container)
})

afterEach(() => {
act(() => {
root.unmount()
})
container.remove()
})

function menuItem(role: string, name: string) {
const item = Array.from(document.querySelectorAll<HTMLElement>(`[role="${role}"]`)).find(
(candidate) => candidate.textContent.includes(name),
)
if (!item) throw new Error(`No ${role} named ${name}`)
return item
}

it('shows no results, not the empty copy, when a status filter matches nothing', async () => {
const api = fakeApi([
{
method: 'GET',
path: `/api/v1/orgs/${orgId}/projects/${projectId}/agents`,
respond: () => jsonResponse({ data: [], next_cursor: null }),
},
])
const client = createOmnaraClient({ baseUrl: 'https://omnara.test/api/v1' })
client.setConfig({ fetch: api.fetch })
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })

act(() => {
root.render(
<OmnaraClientProvider client={client}>
<QueryClientProvider client={queryClient}>
<AgentsSection
orgId={orgId}
projectId={projectId}
canManage
emptyMessage="No agents yet."
emptyAction={<a href="/launch">Launch an agent</a>}
/>
</QueryClientProvider>
</OmnaraClientProvider>,
)
})
await vi.waitFor(() => {
expect(container.textContent).toContain('No agents yet.')
})
expect(container.textContent).toContain('Launch an agent')

const filters = Array.from(container.querySelectorAll('button')).find((button) =>
button.textContent.includes('Filters'),
)
if (!filters) throw new Error('No filters button')
act(() => {
filters.focus()
filters.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
})
act(() => {
menuItem('menuitem', 'Status').click()
})
act(() => {
menuItem('menuitemradio', 'Idle').click()
})

await vi.waitFor(() => {
expect(api.requests.at(-1)?.url.searchParams.get('activity_state')).toBe('idle')
})
expect(container.textContent).toContain('No results.')
expect(container.textContent).not.toContain('No agents yet.')
expect(container.textContent).not.toContain('Launch an agent')
})
23 changes: 20 additions & 3 deletions frontend/apps/web/src/components/agents/AgentsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,26 +36,35 @@ import { type PaginationControls, usePagedQuery } from '@/hooks/use-paged-query'
import { useProjectDirectory } from '@/hooks/use-project-directory'
import { resourceSortOptions, useResourceList } from '@/hooks/use-resource-list'
import { agentIcon, profileIcon } from '@/lib/agent-icon'
import { agentStatusLabel } from '@/lib/agent-status'
import {
activityFilterOptions,
type ActivityFilterValue,
agentStatusLabel,
} from '@/lib/agent-status'

type AgentListControls = ReturnType<typeof useAgentListControls>

function useAgentListControls() {
const list = useResourceList<AgentListSort>('-updated_at')
const [includeSubagents, setIncludeSubagents] = useState(false)
const [includeArchived, setIncludeArchived] = useState(false)
const [activity, setActivity] = useState<ActivityFilterValue>('all')
// Each card's cost, with its subagents, comes with the list rather than a request per card.
const filters: AgentListFilters = { ...list.apiFilters, include_usage: true }
if (includeSubagents) filters.include_subagents = true
if (includeArchived) filters.include_archived = true
if (activity !== 'all') filters.activity_state = activity

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with a status filter selected and no active agent in the results, the list stops polling: useAgents/useOrgAgents only poll while a loaded row is active. e.g. with Waiting selected while agents run, the list is empty, so an agent that later asks a question doesn't show up until something else refetches (a reload, or refocusing the tab after 30s). could the hooks keep polling while a non-archived activity_state filter is set?

return {
list,
filters,
isFiltering: list.isFiltering || activity !== 'all',
includeSubagents,
setIncludeSubagents,
includeArchived,
setIncludeArchived,
resetKey: `${list.queryKey}:${includeSubagents ? 'all' : 'top'}:${includeArchived ? 'archived' : 'active'}`,
activity,
setActivity,
resetKey: `${list.queryKey}:${includeSubagents ? 'all' : 'top'}:${includeArchived ? 'archived' : 'active'}:${activity}`,
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
}
}

Expand Down Expand Up @@ -172,6 +181,14 @@ function AgentList({
showSearch
sort={{ value: list.sort, options: resourceSortOptions, onChange: list.setSort }}
filters={{
activity: {
value: controls.activity,
options: activityFilterOptions,
onChange: (value) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: FiltersMenu already checks the value against the options, so this can be onChange: controls.setActivity

const option = activityFilterOptions.find((candidate) => candidate.value === value)
if (option) controls.setActivity(option.value)
},
},
subagents: {
checked: controls.includeSubagents,
onChange: controls.setIncludeSubagents,
Expand All @@ -192,7 +209,7 @@ function AgentList({
canManage={canManage(agent)}
/>
)}
isFiltered={list.isFiltering}
isFiltered={controls.isFiltering}
pagination={pagination}
isPending={isPending}
isError={isError}
Expand Down
Loading
Loading