feat: Claude Code Monitor — lanes, pipelines and a merged workspace
Internal SmartGift build of a Claude Code monitoring dashboard. Lanes: a durable unit of parallel agent work, one per working directory, tracked across session restarts. Managed lanes are git worktrees the dashboard provisions and can reset or remove behind a three-check destroy guard and a counted preflight; adopted lanes are directories you already own and are never destroyable. Pipelines: a lane moves through pipeline stages. A stage the agent declares with evidence renders green; a stage inferred from the tool-event stream renders dashed amber and never counts as done. Detection is forward-only within a 30-minute window, and never writes the declared stage. Workspace: one page at /run with a lane grid, the selected lane's pipeline, and a full Claude console behind a disclosure.
This commit is contained in:
@@ -0,0 +1,757 @@
|
||||
/**
|
||||
* @file Enriched OVERRIDE fragments for the already-documented Sessions and
|
||||
* Agents endpoints. These paths exist in the base spec (server/openapi.js); the
|
||||
* loader (server/openapi-extra.js) merges `paths` with override-on-key
|
||||
* semantics, so the operations below REPLACE the terser base versions while
|
||||
* preserving their contract: same `operationId`, same `tags`, and the same
|
||||
* request/response `$ref` schema names. The only additions are richer
|
||||
* `description`s and realistic `example`s on every parameter, response media
|
||||
* type, and request body — purely documentation, no contract change.
|
||||
*
|
||||
* No new schemas are defined here (`schemas` is empty by design); everything
|
||||
* reuses the base `components.schemas` and `components.parameters`. Error
|
||||
* responses keep referencing the base `ErrorResponse` ({ error: { code,
|
||||
* message } }). The Sessions/Agents tags are already declared in the base
|
||||
* literal, so `tags` is intentionally empty.
|
||||
*
|
||||
* Covers:
|
||||
* - GET /api/sessions (listSessions)
|
||||
* - POST /api/sessions (createSession)
|
||||
* - GET /api/sessions/{id} (getSession)
|
||||
* - PATCH /api/sessions/{id} (updateSession)
|
||||
* - GET /api/sessions/{id}/stats (getSessionStats)
|
||||
* - GET /api/sessions/{id}/transcripts (listSessionTranscripts)
|
||||
* - GET /api/sessions/{id}/transcript (getSessionTranscript)
|
||||
* - GET /api/agents (listAgents)
|
||||
* - POST /api/agents (createAgent)
|
||||
* - GET /api/agents/{id} (getAgent)
|
||||
* - PATCH /api/agents/{id} (updateAgent)
|
||||
*
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
const tags = [];
|
||||
|
||||
const schemas = {};
|
||||
|
||||
// --- Reusable realistic example fixtures ----------------------------------
|
||||
// Keep these consistent with the route handlers in server/routes/sessions.js
|
||||
// and server/routes/agents.js. Timestamps are ISO-8601 UTC with millisecond
|
||||
// precision; metadata is a raw JSON-encoded string (the DB column is TEXT).
|
||||
|
||||
const exampleSession = {
|
||||
id: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d",
|
||||
name: "Refactor pricing route + add cost endpoint",
|
||||
status: "active",
|
||||
cwd: "/Users/son/WebstormProjects/Claude-Code-Agent-Monitor",
|
||||
model: "claude-opus-4-20250514",
|
||||
started_at: "2026-06-25T14:02:11.004Z",
|
||||
ended_at: null,
|
||||
metadata: '{"source":"hook","git_branch":"feat/spend-budgets"}',
|
||||
updated_at: "2026-06-25T14:31:50.119Z",
|
||||
agent_count: 4,
|
||||
last_activity: "2026-06-25T14:31:50.119Z",
|
||||
cost: 0.8421,
|
||||
awaiting_input_since: null,
|
||||
awaiting_reason: null,
|
||||
};
|
||||
|
||||
const exampleCompletedSession = {
|
||||
id: "1a2b3c4d-5e6f-4071-8293-a4b5c6d7e8f9",
|
||||
name: "Fix flaky transcript pagination test",
|
||||
status: "completed",
|
||||
cwd: "/Users/son/WebstormProjects/Claude-Code-Agent-Monitor",
|
||||
model: "claude-sonnet-4-20250514",
|
||||
started_at: "2026-06-24T09:12:00.000Z",
|
||||
ended_at: "2026-06-24T09:48:32.501Z",
|
||||
metadata: null,
|
||||
updated_at: "2026-06-24T09:48:32.501Z",
|
||||
agent_count: 1,
|
||||
last_activity: "2026-06-24T09:48:32.501Z",
|
||||
cost: 0.1532,
|
||||
awaiting_input_since: null,
|
||||
awaiting_reason: null,
|
||||
};
|
||||
|
||||
const exampleMainAgent = {
|
||||
id: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d-main",
|
||||
session_id: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d",
|
||||
name: "Main Agent",
|
||||
type: "main",
|
||||
subagent_type: null,
|
||||
status: "working",
|
||||
task: null,
|
||||
current_tool: "Edit",
|
||||
started_at: "2026-06-25T14:02:11.004Z",
|
||||
ended_at: null,
|
||||
parent_agent_id: null,
|
||||
metadata: '{"model":"claude-opus-4-20250514"}',
|
||||
updated_at: "2026-06-25T14:31:50.119Z",
|
||||
awaiting_input_since: null,
|
||||
awaiting_reason: null,
|
||||
};
|
||||
|
||||
const exampleSubagent = {
|
||||
id: "ad18a79192af10ed1",
|
||||
session_id: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d",
|
||||
name: "Explore pricing module",
|
||||
type: "subagent",
|
||||
subagent_type: "Explore",
|
||||
status: "completed",
|
||||
task: "Map every caller of calculateCost() across server/routes",
|
||||
current_tool: null,
|
||||
started_at: "2026-06-25T14:10:22.310Z",
|
||||
ended_at: "2026-06-25T14:14:09.882Z",
|
||||
parent_agent_id: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d-main",
|
||||
metadata: null,
|
||||
updated_at: "2026-06-25T14:14:09.882Z",
|
||||
awaiting_input_since: null,
|
||||
awaiting_reason: null,
|
||||
};
|
||||
|
||||
const exampleEvent = {
|
||||
id: 48213,
|
||||
session_id: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d",
|
||||
agent_id: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d-main",
|
||||
event_type: "PostToolUse",
|
||||
tool_name: "Edit",
|
||||
summary: "Edited server/routes/pricing.js",
|
||||
data: '{"tool_input":{"file_path":"server/routes/pricing.js"},"tool_response":{"success":true}}',
|
||||
created_at: "2026-06-25T14:31:50.119Z",
|
||||
};
|
||||
|
||||
const paths = {
|
||||
"/api/sessions": {
|
||||
get: {
|
||||
tags: ["Sessions"],
|
||||
summary: "List sessions",
|
||||
description:
|
||||
"Returns a paginated list of sessions, newest activity first, each enriched with a SQL `agent_count` (LEFT JOIN onto agents), a `last_activity` alias of `updated_at`, and a `cost` computed from the session's token usage against the current pricing rules. The `status` and `q` filters compose (AND) with each other and with pagination; `q` is a case-insensitive LIKE across `id`, `name`, and `cwd`. `total` reflects all rows matching the filters independent of `limit`/`offset` so paginators stay accurate, while `cost` is only calculated for the rows on the returned page (when `sort_by=price` it is computed across all matching rows so the price sort is correct). The endpoint is read-only with no side effects; `metadata` on each session is returned as a raw JSON-encoded string, not a parsed object.",
|
||||
operationId: "listSessions",
|
||||
parameters: [
|
||||
{ $ref: "#/components/parameters/SessionStatusQuery", example: "active" },
|
||||
{
|
||||
name: "q",
|
||||
in: "query",
|
||||
schema: { type: "string" },
|
||||
description:
|
||||
"Case-insensitive search across `id` / `name` / `cwd`. Composes with the status filter when both are present.",
|
||||
example: "pricing",
|
||||
},
|
||||
{
|
||||
$ref: "#/components/parameters/SourcesQuery",
|
||||
example: "local,4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11",
|
||||
},
|
||||
{ $ref: "#/components/parameters/LimitQuery", example: 50 },
|
||||
{ $ref: "#/components/parameters/OffsetQuery", example: 0 },
|
||||
],
|
||||
responses: {
|
||||
200: {
|
||||
description: "Session list",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/SessionsListResponse" },
|
||||
example: {
|
||||
sessions: [exampleSession, exampleCompletedSession],
|
||||
limit: 50,
|
||||
offset: 0,
|
||||
total: 137,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
post: {
|
||||
tags: ["Sessions"],
|
||||
summary: "Create session (idempotent)",
|
||||
description:
|
||||
'Creates a session keyed by `id`. The operation is idempotent: if a session with that `id` already exists it is returned untouched with `created: false` and HTTP 200; only a brand-new row yields `created: true` and HTTP 201. New sessions are inserted with `status: "active"` and any omitted optional fields stored as null. The `metadata` field is accepted as a JSON object in the request but persisted (and returned on the session) as a JSON-encoded string. A successful create broadcasts a `session_created` websocket frame. A missing `id` returns 400 with code `INVALID_INPUT`.',
|
||||
operationId: "createSession",
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/SessionCreateRequest" },
|
||||
example: {
|
||||
id: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d",
|
||||
name: "Refactor pricing route + add cost endpoint",
|
||||
cwd: "/Users/son/WebstormProjects/Claude-Code-Agent-Monitor",
|
||||
model: "claude-opus-4-20250514",
|
||||
metadata: { source: "hook", git_branch: "feat/spend-budgets" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
201: {
|
||||
description: "Session created",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/SessionCreateResponse" },
|
||||
example: {
|
||||
session: {
|
||||
...exampleSession,
|
||||
agent_count: 0,
|
||||
cost: 0,
|
||||
last_activity: "2026-06-25T14:02:11.004Z",
|
||||
updated_at: "2026-06-25T14:02:11.004Z",
|
||||
},
|
||||
created: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
200: {
|
||||
description: "Session already exists",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/SessionCreateResponse" },
|
||||
example: {
|
||||
session: exampleSession,
|
||||
created: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
400: {
|
||||
description: "Invalid request body",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/ErrorResponse" },
|
||||
example: { error: { code: "INVALID_INPUT", message: "id is required" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/sessions/{id}": {
|
||||
get: {
|
||||
tags: ["Sessions"],
|
||||
summary: "Get session details",
|
||||
description:
|
||||
"Returns a single session together with all of its agents (chronological) and persisted events. Read-only, no side effects. The session's `metadata` and each event's `data` are returned as raw JSON-encoded strings, not parsed objects. Returns 404 with code `NOT_FOUND` when no session matches the path `id`.",
|
||||
operationId: "getSession",
|
||||
parameters: [
|
||||
{
|
||||
$ref: "#/components/parameters/SessionIdPath",
|
||||
example: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d",
|
||||
},
|
||||
],
|
||||
responses: {
|
||||
200: {
|
||||
description: "Session with associated agents/events",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/SessionDetailResponse" },
|
||||
example: {
|
||||
session: exampleSession,
|
||||
agents: [exampleMainAgent, exampleSubagent],
|
||||
events: [exampleEvent],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
404: {
|
||||
description: "Session not found",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/ErrorResponse" },
|
||||
example: { error: { code: "NOT_FOUND", message: "Session not found" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
patch: {
|
||||
tags: ["Sessions"],
|
||||
summary: "Update session",
|
||||
description:
|
||||
"Partially updates a session by `id`. Only `name`, `status`, `ended_at`, and `metadata` are accepted; any field omitted from the body is passed as null and the underlying UPDATE uses COALESCE, so a null leaves the existing column value unchanged (partial-update semantics) — you cannot clear a field to null through this endpoint. `metadata` is supplied as a JSON object but stored and returned as a JSON-encoded string. A successful update re-reads the row and broadcasts a `session_updated` websocket frame. Returns 404 with code `NOT_FOUND` when the session does not exist.",
|
||||
operationId: "updateSession",
|
||||
parameters: [
|
||||
{
|
||||
$ref: "#/components/parameters/SessionIdPath",
|
||||
example: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d",
|
||||
},
|
||||
],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/SessionUpdateRequest" },
|
||||
example: {
|
||||
status: "completed",
|
||||
ended_at: "2026-06-25T15:07:44.220Z",
|
||||
metadata: { source: "hook", git_branch: "feat/spend-budgets", outcome: "merged" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Session updated",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/SessionUpdateResponse" },
|
||||
example: {
|
||||
session: {
|
||||
...exampleSession,
|
||||
status: "completed",
|
||||
ended_at: "2026-06-25T15:07:44.220Z",
|
||||
metadata:
|
||||
'{"source":"hook","git_branch":"feat/spend-budgets","outcome":"merged"}',
|
||||
updated_at: "2026-06-25T15:07:44.220Z",
|
||||
last_activity: "2026-06-25T15:07:44.220Z",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
404: {
|
||||
description: "Session not found",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/ErrorResponse" },
|
||||
example: { error: { code: "NOT_FOUND", message: "Session not found" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/sessions/{id}/stats": {
|
||||
get: {
|
||||
tags: ["Sessions"],
|
||||
summary: "Get aggregated session stats",
|
||||
description:
|
||||
"Returns aggregated counts for the SessionOverview panel: total events, events-by-type, the top 15 tools by usage, an error count (events whose `event_type`/`summary` match /error/i or /failed/i), the event time range, agent type/status counts, the subagent-type breakdown (excluding the special `compaction` type, which is surfaced under `agents.compaction`), and token totals. All aggregation runs in SQL, so it stays cheap even for sessions with tens of thousands of events; the endpoint is read-only with no side effects. The frontend debounces calls on `new_event` / `agent_*` / `session_updated` websocket frames so the counters track a running session. Returns 404 with code `NOT_FOUND` when the session does not exist.",
|
||||
operationId: "getSessionStats",
|
||||
parameters: [
|
||||
{
|
||||
$ref: "#/components/parameters/SessionIdPath",
|
||||
example: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d",
|
||||
},
|
||||
],
|
||||
responses: {
|
||||
200: {
|
||||
description: "Aggregated session stats",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/SessionStatsResponse" },
|
||||
example: {
|
||||
session_id: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d",
|
||||
total_events: 1284,
|
||||
events_by_type: [
|
||||
{ event_type: "PostToolUse", count: 612 },
|
||||
{ event_type: "PreToolUse", count: 612 },
|
||||
{ event_type: "Notification", count: 41 },
|
||||
{ event_type: "Stop", count: 19 },
|
||||
],
|
||||
tools_used: [
|
||||
{ tool_name: "Bash", count: 188 },
|
||||
{ tool_name: "Edit", count: 143 },
|
||||
{ tool_name: "Read", count: 121 },
|
||||
{ tool_name: "Grep", count: 77 },
|
||||
],
|
||||
error_count: 6,
|
||||
first_event_at: "2026-06-25T14:02:11.052Z",
|
||||
last_event_at: "2026-06-25T14:31:50.119Z",
|
||||
agents: {
|
||||
total: 4,
|
||||
main: 1,
|
||||
subagent: 3,
|
||||
compaction: 1,
|
||||
by_status: { working: 1, completed: 2, error: 1 },
|
||||
},
|
||||
subagent_types: [
|
||||
{ subagent_type: "Explore", count: 2 },
|
||||
{ subagent_type: "general-purpose", count: 1 },
|
||||
],
|
||||
tokens: {
|
||||
input_tokens: 18422,
|
||||
output_tokens: 9134,
|
||||
cache_read_tokens: 1204880,
|
||||
cache_write_tokens: 88210,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
404: {
|
||||
description: "Session not found",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/ErrorResponse" },
|
||||
example: { error: { code: "NOT_FOUND", message: "Session not found" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/sessions/{id}/transcripts": {
|
||||
get: {
|
||||
tags: ["Sessions"],
|
||||
summary: "List available transcripts for a session",
|
||||
description:
|
||||
"Lists every JSONL transcript file associated with a session — the main agent's transcript plus any subagent and compaction transcripts — by scanning the on-disk Claude project directory (live files, falling back to import-time snapshots). Read-only, no side effects. Each entry carries a best-effort `db_agent_id` resolved by matching transcripts to tracked agents (exact id first, then positional-by-time within each type group); it may be null when a transcript has no matching agent row. Used by the Conversation tab to populate the transcript switcher. Returns 404 with code `NOT_FOUND` when the session does not exist.",
|
||||
operationId: "listSessionTranscripts",
|
||||
parameters: [
|
||||
{
|
||||
$ref: "#/components/parameters/SessionIdPath",
|
||||
example: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d",
|
||||
},
|
||||
],
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of transcripts available for the session",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/TranscriptListResponse" },
|
||||
example: {
|
||||
transcripts: [
|
||||
{
|
||||
id: "main",
|
||||
name: "Main Agent",
|
||||
type: "main",
|
||||
has_transcript: true,
|
||||
db_agent_id: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d-main",
|
||||
},
|
||||
{
|
||||
id: "ad18a79192af10ed1",
|
||||
name: "Explore pricing module",
|
||||
type: "subagent",
|
||||
subagent_type: "Explore",
|
||||
has_transcript: true,
|
||||
db_agent_id: "ad18a79192af10ed1",
|
||||
},
|
||||
{
|
||||
id: "acompact-7c1e2f90",
|
||||
name: "Context Compaction",
|
||||
type: "compaction",
|
||||
subagent_type: null,
|
||||
has_transcript: true,
|
||||
db_agent_id: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
404: {
|
||||
description: "Session not found",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/ErrorResponse" },
|
||||
example: { error: { code: "NOT_FOUND", message: "Session not found" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/sessions/{id}/transcript": {
|
||||
get: {
|
||||
tags: ["Sessions"],
|
||||
summary: "Stream messages from a specific transcript",
|
||||
description:
|
||||
"Returns parsed, renderable messages from a JSONL transcript with cursor-based pagination, reading the live file under ~/.claude/projects and falling back to the durable import-time snapshot. Pass `agent_id` to select a specific subagent or compaction transcript (default is the session's main transcript). Pagination cursors are mutually exclusive: `after` returns messages strictly newer than a JSONL line number (incremental live updates on `new_event`), `before` returns messages strictly older than a line (load-on-scroll-up), and `offset` is legacy start-offset paging. `last_line`/`first_line` are the JSONL line numbers of the newest/oldest returned message — feed them back as `after`/`before`. When the session, transcript file, or path cannot be found the endpoint degrades gracefully to an empty result (`messages: []`, `total: 0`, `has_more: false`) rather than erroring. Read-only, no side effects.",
|
||||
operationId: "getSessionTranscript",
|
||||
parameters: [
|
||||
{
|
||||
$ref: "#/components/parameters/SessionIdPath",
|
||||
example: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d",
|
||||
},
|
||||
{
|
||||
name: "agent_id",
|
||||
in: "query",
|
||||
schema: { type: "string" },
|
||||
description:
|
||||
"Transcript identifier — 'main' for the session's main transcript, or a subagent / compaction id from /transcripts.",
|
||||
example: "main",
|
||||
},
|
||||
{
|
||||
name: "limit",
|
||||
in: "query",
|
||||
schema: { type: "integer", default: 50, minimum: 1, maximum: 500 },
|
||||
description: "Maximum number of messages to return.",
|
||||
example: 50,
|
||||
},
|
||||
{
|
||||
name: "offset",
|
||||
in: "query",
|
||||
schema: { type: "integer", minimum: 0 },
|
||||
description:
|
||||
"Offset from the start of the transcript (mutually exclusive with after/before).",
|
||||
example: 0,
|
||||
},
|
||||
{
|
||||
name: "after",
|
||||
in: "query",
|
||||
schema: { type: "integer", minimum: 0 },
|
||||
description:
|
||||
"Only return messages whose JSONL line number is strictly greater than this value. Used for incremental live updates.",
|
||||
example: 842,
|
||||
},
|
||||
{
|
||||
name: "before",
|
||||
in: "query",
|
||||
schema: { type: "integer", minimum: 0 },
|
||||
description:
|
||||
"Only return messages whose JSONL line number is strictly less than this value. Used to load older messages on scroll-up.",
|
||||
example: 200,
|
||||
},
|
||||
],
|
||||
responses: {
|
||||
200: {
|
||||
description: "Parsed messages with cursor metadata",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/TranscriptResponse" },
|
||||
example: {
|
||||
messages: [
|
||||
{
|
||||
type: "user",
|
||||
timestamp: "2026-06-25T14:02:11.004Z",
|
||||
content: [
|
||||
{ type: "text", text: "Refactor the pricing route and add a cost endpoint." },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "assistant",
|
||||
timestamp: "2026-06-25T14:02:18.771Z",
|
||||
model: "claude-opus-4-20250514",
|
||||
content: [
|
||||
{ type: "thinking", text: "I'll start by reading server/routes/pricing.js." },
|
||||
{
|
||||
type: "tool_use",
|
||||
name: "Read",
|
||||
id: "toolu_01A7c2Df9",
|
||||
input: { file_path: "server/routes/pricing.js" },
|
||||
},
|
||||
],
|
||||
usage: { input_tokens: 412, output_tokens: 96 },
|
||||
},
|
||||
{
|
||||
type: "user",
|
||||
timestamp: "2026-06-25T14:02:19.330Z",
|
||||
content: [
|
||||
{
|
||||
type: "tool_result",
|
||||
id: "toolu_01A7c2Df9",
|
||||
output: 'const { Router } = require("express");\n...',
|
||||
is_error: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
total: 1284,
|
||||
has_more: true,
|
||||
last_line: 5310,
|
||||
first_line: 5301,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
404: {
|
||||
description: "Session or transcript not found",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/ErrorResponse" },
|
||||
example: { error: { code: "NOT_FOUND", message: "Session not found" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/agents": {
|
||||
get: {
|
||||
tags: ["Agents"],
|
||||
summary: "List agents",
|
||||
description:
|
||||
"Returns agents, most recent first. Filters are applied with precedence rather than composition: when `session_id` is supplied it wins and returns every agent for that session (ignoring `status` and pagination); otherwise a `status` filter returns paginated agents in that lifecycle state; otherwise all agents are returned paginated. `limit` defaults to 10000 when not a positive integer. Read-only, no side effects. Each agent's `metadata` is returned as a raw JSON-encoded string, not a parsed object.",
|
||||
operationId: "listAgents",
|
||||
parameters: [
|
||||
{ $ref: "#/components/parameters/AgentStatusQuery", example: "working" },
|
||||
{
|
||||
$ref: "#/components/parameters/SessionFilterQuery",
|
||||
example: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d",
|
||||
},
|
||||
{
|
||||
$ref: "#/components/parameters/SourcesQuery",
|
||||
example: "local,4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11",
|
||||
},
|
||||
{ $ref: "#/components/parameters/LimitQuery", example: 50 },
|
||||
{ $ref: "#/components/parameters/OffsetQuery", example: 0 },
|
||||
],
|
||||
responses: {
|
||||
200: {
|
||||
description: "Agent list",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/AgentsListResponse" },
|
||||
example: {
|
||||
agents: [exampleMainAgent, exampleSubagent],
|
||||
limit: 50,
|
||||
offset: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
post: {
|
||||
tags: ["Agents"],
|
||||
summary: "Create agent (idempotent)",
|
||||
description:
|
||||
'Creates an agent keyed by `id`. The operation is idempotent: if an agent with that `id` already exists it is returned untouched with `created: false` and HTTP 200; only a brand-new row yields `created: true` and HTTP 201. Omitted optional fields default server-side — `type` to `"main"`, `status` to `"waiting"` — and other unspecified columns are stored as null. `metadata` is accepted as a JSON object but persisted (and returned) as a JSON-encoded string. A successful create broadcasts an `agent_created` websocket frame. Missing `id`, `session_id`, or `name` returns 400 with code `INVALID_INPUT`.',
|
||||
operationId: "createAgent",
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/AgentCreateRequest" },
|
||||
example: {
|
||||
id: "ad18a79192af10ed1",
|
||||
session_id: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d",
|
||||
name: "Explore pricing module",
|
||||
type: "subagent",
|
||||
subagent_type: "Explore",
|
||||
status: "working",
|
||||
task: "Map every caller of calculateCost() across server/routes",
|
||||
parent_agent_id: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d-main",
|
||||
metadata: { spawned_by: "Task" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
201: {
|
||||
description: "Agent created",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/AgentCreateResponse" },
|
||||
example: {
|
||||
agent: {
|
||||
...exampleSubagent,
|
||||
status: "working",
|
||||
ended_at: null,
|
||||
metadata: '{"spawned_by":"Task"}',
|
||||
updated_at: "2026-06-25T14:10:22.310Z",
|
||||
},
|
||||
created: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
200: {
|
||||
description: "Agent already exists",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/AgentCreateResponse" },
|
||||
example: {
|
||||
agent: exampleSubagent,
|
||||
created: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
400: {
|
||||
description: "Invalid request body",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/ErrorResponse" },
|
||||
example: {
|
||||
error: { code: "INVALID_INPUT", message: "id, session_id, and name are required" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/agents/{id}": {
|
||||
get: {
|
||||
tags: ["Agents"],
|
||||
summary: "Get agent",
|
||||
description:
|
||||
"Returns a single agent by `id`. Read-only, no side effects. The agent's `metadata` is returned as a raw JSON-encoded string, not a parsed object. Returns 404 with code `NOT_FOUND` when no agent matches the path `id`.",
|
||||
operationId: "getAgent",
|
||||
parameters: [{ $ref: "#/components/parameters/AgentIdPath", example: "ad18a79192af10ed1" }],
|
||||
responses: {
|
||||
200: {
|
||||
description: "Agent details",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/AgentDetailResponse" },
|
||||
example: { agent: exampleSubagent },
|
||||
},
|
||||
},
|
||||
},
|
||||
404: {
|
||||
description: "Agent not found",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/ErrorResponse" },
|
||||
example: { error: { code: "NOT_FOUND", message: "Agent not found" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
patch: {
|
||||
tags: ["Agents"],
|
||||
summary: "Update agent",
|
||||
description:
|
||||
"Partially updates an agent by `id`. Accepts `name`, `status`, `task`, `current_tool`, `ended_at`, and `metadata`. The UPDATE uses COALESCE, so any field omitted (passed as null) leaves the existing column value unchanged — with one deliberate exception: `current_tool` is written through verbatim when present in the body, so it can be explicitly cleared to null (e.g. when a tool call finishes). `metadata` is supplied as a JSON object but stored and returned as a JSON-encoded string. A successful update re-reads the row and broadcasts an `agent_updated` websocket frame. Returns 404 with code `NOT_FOUND` when the agent does not exist.",
|
||||
operationId: "updateAgent",
|
||||
parameters: [{ $ref: "#/components/parameters/AgentIdPath", example: "ad18a79192af10ed1" }],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/AgentUpdateRequest" },
|
||||
example: {
|
||||
status: "completed",
|
||||
current_tool: null,
|
||||
ended_at: "2026-06-25T14:14:09.882Z",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Agent updated",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/AgentUpdateResponse" },
|
||||
example: {
|
||||
agent: {
|
||||
...exampleSubagent,
|
||||
status: "completed",
|
||||
current_tool: null,
|
||||
ended_at: "2026-06-25T14:14:09.882Z",
|
||||
updated_at: "2026-06-25T14:14:09.882Z",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
404: {
|
||||
description: "Agent not found",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/ErrorResponse" },
|
||||
example: { error: { code: "NOT_FOUND", message: "Agent not found" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = { tags, schemas, paths };
|
||||
Reference in New Issue
Block a user