Agent Session Reporting
Read agent-session usage and analytics programmatically
Overview
Agent-session reporting tools expose the same usage and analytics data that powers the Closedloop dashboard, so AI clients can pull cost, token, and activity metrics programmatically.
These tools are read-only and operate across the sessions you are authorized to see. list-agent-sessions enumerates individual session rows, while get-agent-session-usage and get-agent-session-analytics return aggregated reporting data. The API enforces viewer scoping (self / team / organization) server-side, so you only ever receive data your account is permitted to view.
Agent-session tools also require monitoring access. If monitoring is disabled for your account or organization, the API returns 403 even when the MCP token has read scope.
Shared Filters
The two reporting tools (get-agent-session-usage and get-agent-session-analytics) accept the same optional filters. Omit them to aggregate over every substantive session in the widest scope you are authorized for — idle sessions (0 turns, 0 tokens, and 0 tool uses) are excluded by default; pass quality: "all" to include them, or quality: "idle" to aggregate only idle sessions. list-agent-sessions accepts a superset of these filters (see its section below).
startDate?: string // ISO 8601 — analytics: sessions STARTED on/after; usage: sessions ACTIVE on/after (e.g. 2026-07-01)
endDate?: string // ISO 8601 — analytics: sessions STARTED on/before; usage: sessions ACTIVE on/before (e.g. 2026-07-31)
harness?: string // Single agent harness (e.g. "claude-code", "codex")
quality?: string // "substantive" | "idle" | "all" — defaults to "substantive" (excludes idle 0-turn/0-token/0-tool sessions)
viewerScope?: string // "self" | "organization" | "team"
teamId?: string // UUID — required when viewerScope is "team", must be omitted otherwiseteamId is coupled to viewerScope: it is required when viewerScope is "team" (aggregating over that team), and must be omitted for any other scope. Passing viewerScope: "team" without a teamId, or a teamId with a non-team scope, returns a server validation error.
Tool Reference
List Agent Sessions
Tool: list-agent-sessions
Underlying API: GET /agent-sessions
Enumerates individual agent sessions — one AI agent run each — with harness, model, repository, token usage, cost, and status. It accepts the shared filters plus several list-specific ones:
list-agent-sessions(
startDate?: string, // ISO 8601 date/timestamp — activity window on lastActivityAt (on or after)
endDate?: string, // ISO 8601 date/timestamp — activity window on lastActivityAt (on or before)
harness?: string, // Single agent harness (e.g. "claude-code", "codex")
status?: string, // "active" | "waiting" | "inactive" | "error" | "stale" | "unknown" — "inactive" covers finished (non-failed) runs
userId?: string, // UUID — filter to a single user's sessions
quality?: string, // "substantive" | "idle" | "all" — defaults to "substantive" (hides idle 0-turn/0-token/0-tool sessions)
viewerScope?: string, // "self" | "organization" | "team"
teamId?: string, // UUID — required when viewerScope is "team", omit otherwise
limit?: number, // Maximum sessions to return
offset?: number // Starting offset for pagination (default 0)
)startDate/endDate here are an activity window on lastActivityAt, not a session start-date filter: it keeps the sessions whose lastActivityAt falls within the window. So a session that started before startDate is still included if its last activity landed inside the window — but one that was active during the window and then active again after endDate is excluded, because only that final lastActivityAt is tested. get-agent-session-usage windows on that same basis, but it accepts neither status nor userId — only the shared filters — so its totals reconcile with this list only when the list is narrowed by the shared filters alone. Narrow the list by status or userId and the usage totals still include the rows the list filtered out. Only get-agent-session-analytics is a session start-date filter (on sessionStartedAt).
The status values above are what you may send as a filter. A returned session's status is not simply the raw stored value: the server derives it at read time, so a run stored as active can come back as waiting (awaiting user input on a run that has not ended) or as stale (silent past the display staleness cutoff), and a stored value the server does not recognize generally comes back as unknown — the awaiting-input derivation runs first, so even an unrecognized row can surface as waiting. Where no derivation applies the stored value passes through unchanged. A retired completed/abandoned from a not-yet-migrated row is no longer recognized (ISS-5592), so it comes back as unknown like any other unrecognized spelling. So do not assume a returned status is one of the filter values above, nor that re-sending a returned value as a filter always finds the row that carried it.
Get Agent-Session Usage
Tool: get-agent-session-usage
Underlying API: GET /agent-sessions/usage
get-agent-session-usage(
startDate?: string, // ISO 8601 — activity window on lastActivityAt (last active on or after), same basis as list-agent-sessions; tests the final lastActivityAt, not any activity in the window
endDate?: string, // ISO 8601 — activity window on lastActivityAt (last active on or before)
harness?: string,
quality?: string, // "substantive" | "idle" | "all" — defaults to "substantive" (excludes idle 0-turn/0-token/0-tool sessions)
viewerScope?: string,
teamId?: string // UUID — required when viewerScope is "team", omit otherwise
)Returns an aggregated usage summary: total session count, input/output/cache token totals, estimated cost split into subscription-covered vs. API-key compute, and breakdowns by user, model, harness, and repository.
Get Agent-Session Analytics
Tool: get-agent-session-analytics
Underlying API: GET /agent-sessions/analytics
get-agent-session-analytics(
startDate?: string, // ISO 8601 — session start window on sessionStartedAt (started on or after)
endDate?: string, // ISO 8601 — session start window on sessionStartedAt (started on or before)
harness?: string,
quality?: string, // "substantive" | "idle" | "all" — defaults to "substantive" (excludes idle 0-turn/0-token/0-tool sessions)
viewerScope?: string,
teamId?: string // UUID — required when viewerScope is "team", omit otherwise
)Returns analytics breakdowns by tool, agent type, repository, and project.
Get Agent-Session Transcript
Tool: get-agent-session-transcript
Underlying API: GET /agent-sessions/{sessionId}/transcript
Get read access to an agent session's archived transcript files — the main run file plus any subagent sidechain files. Read-only.
get-agent-session-transcript(
sessionId: string // UUID — the `id` field returned by list-agent-sessions (NOT the SES-* slug)
)The sessionId must be the artifact id UUID (the id field on a list-agent-sessions row), not the human-facing SES-* slug — the slug is not accepted here.
The response returns one descriptor per transcript file:
{
"sessionId": "...",
"files": [
{
"fileKey": "main",
"availability": "available",
"url": "https://...signed-s3-get...",
"byteSize": 48213,
"rawSha256": "...",
"uploadedAt": "2026-07-01T10:00:00Z",
"lastObservedAt": "2026-07-01T10:00:05Z",
"permanentFailureReason": null
}
]
}fileKey is main for the primary run file or subagent:{fileId} for a subagent sidechain. The signed url is a short-lived S3 GET (expires in ~5 minutes) and is non-null only when availability is available or stale; for other states (uploadPending, uploadFailed, permanentlyUnavailable, missing) it is null. Fetch the url directly to read the raw JSONL trajectory, and mint a fresh call if the URL expires before you finish.
List Session Comments
Tool: list-session-comments
Underlying API: GET /agent-sessions/{sessionId}/trace-comments
List read-only trace comments for one agent session. The tool uses the same session visibility and organization authorization as the other Agent Sessions tools.
list-session-comments(
sessionId?: string, // UUID — the `id` from list-agent-sessions; SES-* slugs are not accepted
sessionUrl?: string, // canonical Closedloop URL ending in /sessions/{uuid}; use instead of sessionId
computeTargetId?: string, // optional UUID for desktop-local session resolution
status?: string, // "OPEN" | "RESOLVED"
limit?: number, // maximum comments to return
offset?: number // starting offset for pagination (default 0)
)Provide exactly one of sessionId or sessionUrl. The tool is read-only: it does not create, edit, resolve, delete, notify, or mutate comments.
{
"total": 1,
"offset": 0,
"limit": 25,
"returned": 1,
"hasMore": false,
"nextOffset": null,
"items": [
{
"id": "comment-1",
"threadId": "thread-1",
"body": "Check this trace span.",
"status": "RESOLVED",
"resolvedAt": "2026-07-20T10:00:00.000Z",
"resolvedById": "user-resolver",
"resolvedByName": "Resolver User",
"resolvedByAvatarUrl": null,
"target": { "type": "session", "id": "11111111-1111-4111-8111-111111111111" },
"artifactId": "11111111-1111-4111-8111-111111111111",
"anchor": {
"anchorType": "text",
"traceId": "trace-1",
"turnId": "turn-1",
"row": 3,
"selectedText": "selected text",
"sourceText": "source selected text",
"startOffset": 7,
"endOffset": 20,
"sessionId": "11111111-1111-4111-8111-111111111111",
"actor": { "name": "codex", "human": null }
},
"createdAt": "2026-07-20T09:00:00.000Z",
"updatedAt": "2026-07-20T09:01:00.000Z",
"editedAt": null,
"authorId": "user-author",
"authorName": "Author User",
"authorAvatarUrl": null,
"replies": [
{
"id": "reply-1",
"threadId": "thread-1",
"body": "Follow-up note.",
"createdAt": "2026-07-20T09:02:00.000Z",
"updatedAt": "2026-07-20T09:03:00.000Z",
"editedAt": null,
"authorId": "user-reply",
"authorName": "Reply User",
"authorAvatarUrl": null
}
]
}
]
}resolvedAt, resolvedById, resolvedByName, and resolvedByAvatarUrl are nullable; open threads and older rows without resolver metadata return null. anchor.anchorType is always "text" today because trace comments use text-selection anchors.
Pagination is offset-based over a fresh authorized API read for each call. Ordering is deterministic while the source set is unchanged; if comments change between calls and you need a complete refreshed traversal, restart at offset: 0.
Example
# Usage for July 2026, organization-wide
get-agent-session-usage(
startDate: "2026-07-01",
endDate: "2026-07-31",
viewerScope: "organization"
)
# Tool/agent-type analytics for a single harness
get-agent-session-analytics(harness: "claude-code")
# Usage for a single team (viewerScope "team" requires teamId)
get-agent-session-usage(
viewerScope: "team",
teamId: "00000000-0000-0000-0000-000000000000"
)
# Pull one run's transcript files (pass the row `id`, not the SES-* slug)
list-agent-sessions(limit: 1)
→ take the first item's `id`
get-agent-session-transcript(sessionId: "<id-from-list-agent-sessions>")
→ fetch each non-null `url` directly to read the raw JSONL trajectory
# Read open trace comments for one run
list-session-comments(
sessionId: "<id-from-list-agent-sessions>",
status: "OPEN",
limit: 25
)
# Equivalent URL form
list-session-comments(
sessionUrl: "https://app.closedloop.ai/acme/sessions/11111111-1111-4111-8111-111111111111"
)