feat: include agent metadata in workspace list responses (#27934)

Closes #27933. Related: #27897 (single-agent GET).

Agent metadata is only readable via a per-agent watch stream, so reading
it across N workspaces costs N+1 requests. This adds a batch read to the
list endpoint:

```text
GET /api/v2/workspaces?q=param:"pool=demo" include_agent_metadata:task_status
```

- New `include_agent_metadata` search key, repeatable and key-scoped. It
expands the response, it does not filter workspaces.
- `GetWorkspaces` aggregates the requested keys as JSON behind a `CASE`:
without opt-in the response is unchanged and the subquery never runs.
Runs only for the returned page, inside the same authorized query.
- Agents in the response gain `metadata`
(`[]codersdk.WorkspaceAgentMetadata`, `omitempty`), mapped by the
`workspace_agent_id` each element carries. The collection script is
omitted; it can be long.
- `codersdk.WorkspaceFilter` gains `IncludeAgentMetadata []string`.
- No wildcard, no schema change, no migration.

---

Authored by Coder Agents on behalf of @Emyrk.
This commit is contained in:
Steven Masley
2026-08-10 08:13:32 -05:00
committed by GitHub
parent a3a51228ee
commit 9a57dfa642
21 changed files with 1507 additions and 588 deletions
+28
View File
@@ -120,6 +120,34 @@ type ChatACLEntry struct {
Permissions []policy.Action `json:"permissions"`
}
// AgentMetadataAggregate is the agent_metadata jsonb array the
// GetWorkspaces query aggregates for the include_agent_metadata
// expansion. Elements have WorkspaceAgentMetadatum's JSON shape; each
// carries its workspace_agent_id so multi-agent workspaces can map
// values onto the right agent. The generated row keeps
// json.RawMessage because sqlc overrides cannot target expression
// columns; callers Scan the raw value into this type.
type AgentMetadataAggregate []WorkspaceAgentMetadatum
func (a *AgentMetadataAggregate) Scan(src interface{}) error {
switch v := src.(type) {
case nil:
return nil
case string:
return json.Unmarshal([]byte(v), &a)
case []byte:
return json.Unmarshal(v, &a)
case json.RawMessage:
return json.Unmarshal(v, &a)
}
return xerrors.Errorf("unexpected type %T", src)
}
func (a AgentMetadataAggregate) Value() (driver.Value, error) {
return json.Marshal(a)
}
type WorkspaceACL map[string]WorkspaceACLEntry
func (t *WorkspaceACL) Scan(src interface{}) error {