diff --git a/internal/cmd/gen/main.go b/internal/cmd/gen/main.go
index 529f5e4..5abd873 100644
--- a/internal/cmd/gen/main.go
+++ b/internal/cmd/gen/main.go
@@ -162,6 +162,13 @@ func (g *Gen) collectServices(paths map[string]any) []service {
if len(tags) == 0 {
continue
}
+ // Skip streaming endpoints: their 200 response is not application/json
+ // (e.g. application/x-ndjson). The typed do/doGet path reads the whole
+ // body and JSON-decodes one envelope, which is wrong for a line-delimited
+ // stream. These get a hand-written method instead (see sessions_export.go).
+ if isStreamingOp(o) {
+ continue
+ }
tag, _ := tags[0].(string)
byTag[tag] = append(byTag[tag], opEntry{p, m, o})
}
@@ -209,6 +216,21 @@ func (g *Gen) collectServices(paths map[string]any) []service {
return services
}
+// isStreamingOp reports whether an operation's 200 response is a non-JSON
+// streaming body (its 200 content has no "application/json" key). Such endpoints
+// — e.g. session/export returning application/x-ndjson — cannot be modeled by the
+// typed do/doGet path (which buffers the body and decodes one JSON envelope) and
+// are excluded from generation in favor of a hand-written streaming method.
+func isStreamingOp(o map[string]any) bool {
+ resp := asMap(asMap(o["responses"])["200"])
+ content := asMap(resp["content"])
+ if len(content) == 0 {
+ return false // no body at all is not a streaming response
+ }
+ _, hasJSON := content["application/json"]
+ return !hasJSON
+}
+
// getRequestType synthesizes a request struct from a GET op's query parameters
// and records it for emission. Returns "" when the op has no query parameters.
func (g *Gen) getRequestType(o map[string]any, hint string) string {
diff --git a/models_gen.go b/models_gen.go
index 15cef9c..766ff63 100644
--- a/models_gen.go
+++ b/models_gen.go
@@ -1424,6 +1424,20 @@ type CommentIncidentRequest struct {
MuteReply bool `json:"mute_reply,omitempty" toon:"mute_reply,omitempty"`
}
+// ContextResolvedItem is generated from the Flashduty OpenAPI schema.
+type ContextResolvedItem struct {
+ // Resolved account-scoped pack id.
+ AccountPackID string `json:"account_pack_id" toon:"account_pack_id"`
+ // Bound incident id, when war-room originated.
+ IncidentID string `json:"incident_id" toon:"incident_id"`
+ // Unix timestamp in milliseconds when the packs were resolved.
+ ResolvedAtMs TimestampMilli `json:"resolved_at_ms" toon:"resolved_at_ms"`
+ // Resolved team-scoped pack id.
+ TeamPackID string `json:"team_pack_id" toon:"team_pack_id"`
+ // Per-pack resolved version map.
+ Versions map[string]int64 `json:"versions" toon:"versions"`
+}
+
// CreateChannelRequest is generated from the Flashduty OpenAPI schema.
type CreateChannelRequest struct {
// Auto-resolve timer reset mode.
@@ -2085,6 +2099,18 @@ type EnrichmentUpsertRequest struct {
Rules []EnrichRule `json:"rules,omitempty" toon:"rules,omitempty"`
}
+// EnvironmentBinding is generated from the Flashduty OpenAPI schema.
+type EnvironmentBinding struct {
+ // Environment identifier.
+ ID string `json:"id" toon:"id"`
+ // Environment kind (e.g. runner, sandbox).
+ Kind string `json:"kind" toon:"kind"`
+ // Human-readable environment name.
+ Name string `json:"name" toon:"name"`
+ // Binding status.
+ Status string `json:"status" toon:"status"`
+}
+
// ErsComposition is generated from the Flashduty OpenAPI schema.
type ErsComposition struct {
// When `true`, overwrite the label if it already exists. Defaults to `false`.
@@ -2195,6 +2221,85 @@ type EscalateTarget struct {
Webhooks []EscalateTargetWebhooksItem `json:"webhooks,omitempty" toon:"webhooks,omitempty"`
}
+// EventItem is generated from the Flashduty OpenAPI schema.
+type EventItem struct {
+ // ADK actions envelope (state deltas, transfers, escalation).
+ Actions map[string]any `json:"actions" toon:"actions"`
+ // Event author (e.g. user, the agent name).
+ Author string `json:"author" toon:"author"`
+ // ADK branch path for nested agents.
+ Branch string `json:"branch" toon:"branch"`
+ // ADK content envelope {role, parts:[...]}.
+ Content map[string]any `json:"content" toon:"content"`
+ // Unix timestamp in milliseconds when the event was written.
+ CreatedAt TimestampMilli `json:"created_at" toon:"created_at"`
+ // Error code when the event represents a failure.
+ ErrorCode string `json:"error_code" toon:"error_code"`
+ // Human-readable error message, when present.
+ ErrorMessage string `json:"error_message" toon:"error_message"`
+ // Event identifier.
+ EventID string `json:"event_id" toon:"event_id"`
+ // ADK invocation id grouping a turn.
+ InvocationID string `json:"invocation_id" toon:"invocation_id"`
+ // True for a streaming partial chunk.
+ Partial bool `json:"partial" toon:"partial"`
+ // Owning session id.
+ SessionID string `json:"session_id" toon:"session_id"`
+ // Event status.
+ Status string `json:"status" toon:"status"`
+ // True on the terminal event of a turn.
+ TurnComplete bool `json:"turn_complete" toon:"turn_complete"`
+ // Per-turn token usage metadata.
+ UsageMetadata map[string]any `json:"usage_metadata" toon:"usage_metadata"`
+}
+
+// ExportLine is generated from the Flashduty OpenAPI schema.
+type ExportLine struct {
+ // Account id (on session_meta).
+ AccountID int64 `json:"account_id" toon:"account_id"`
+ // Dispatched subagent name (on subagent_dispatch).
+ AgentName string `json:"agent_name" toon:"agent_name"`
+ // Agent app (on session_meta).
+ AppName string `json:"app_name" toon:"app_name"`
+ // Child session id created by the dispatch (on subagent_dispatch).
+ ChildSessionID string `json:"child_session_id" toon:"child_session_id"`
+ // Text content of the line (messages, answers, errors).
+ Content string `json:"content" toon:"content"`
+ // Call duration in milliseconds.
+ DurationMs int64 `json:"duration_ms" toon:"duration_ms"`
+ // RFC3339 end timestamp; stamped on llm_call/tool_call/session_meta.
+ EndedAt string `json:"ended_at" toon:"ended_at"`
+ // Error detail when a call failed.
+ Error string `json:"error" toon:"error"`
+ // Tool call input arguments (on tool_call).
+ Input map[string]any `json:"input" toon:"input"`
+ // Byte size of the tool input.
+ InputBytes int64 `json:"input_bytes" toon:"input_bytes"`
+ // Chat model provider key; on session_meta and llm_call.
+ Model string `json:"model" toon:"model"`
+ // Tool name (on tool_call).
+ Name string `json:"name" toon:"name"`
+ // Tool call output (on tool_call response side).
+ Output string `json:"output" toon:"output"`
+ // Byte size of the tool output.
+ OutputBytes int64 `json:"output_bytes" toon:"output_bytes"`
+ // Parent session id for child sessions (on session_meta).
+ ParentSessionID string `json:"parent_session_id" toon:"parent_session_id"`
+ // 1-based monotonic sequence within the session (absent on session_meta).
+ Seq int64 `json:"seq" toon:"seq"`
+ // Session id (on session_meta).
+ SessionID string `json:"session_id" toon:"session_id"`
+ // RFC3339 start timestamp (session_meta uses session.created_at).
+ StartedAt string `json:"started_at" toon:"started_at"`
+ // Tool result status, e.g. ok or error.
+ Status string `json:"status" toon:"status"`
+ // RFC3339 timestamp of the event.
+ TS string `json:"ts" toon:"ts"`
+ // Line discriminator.
+ Type string `json:"type" toon:"type"`
+ Usage ExportUsage `json:"usage" toon:"usage"`
+}
+
// ExportStatusPageSubscribersRequest is generated from the Flashduty OpenAPI schema.
type ExportStatusPageSubscribersRequest struct {
// Optional component IDs to filter subscribers by.
@@ -2203,6 +2308,18 @@ type ExportStatusPageSubscribersRequest struct {
PageID int64 `json:"page_id,omitempty" toon:"page_id,omitempty"`
}
+// ExportUsage is generated from the Flashduty OpenAPI schema.
+type ExportUsage struct {
+ // Tokens written to the prompt cache.
+ CacheCreation int64 `json:"cache_creation" toon:"cache_creation"`
+ // Tokens served from the prompt cache.
+ CacheRead int64 `json:"cache_read" toon:"cache_read"`
+ // Prompt (input) tokens for the call.
+ InputTokens int64 `json:"input_tokens" toon:"input_tokens"`
+ // Generated (output) tokens for the call.
+ OutputTokens int64 `json:"output_tokens" toon:"output_tokens"`
+}
+
// ExportedStatusPageSubscriberItem is generated from the Flashduty OpenAPI schema.
type ExportedStatusPageSubscriberItem struct {
// Whether the subscriber is subscribed to all components.
@@ -5298,6 +5415,133 @@ type ScheduleUpsertRequest struct {
TeamID *int64 `json:"team_id,omitempty" toon:"team_id,omitempty"`
}
+// SessionExportRequest is generated from the Flashduty OpenAPI schema.
+type SessionExportRequest struct {
+ // When true, each subagent_dispatch line is followed by the child session's full event stream, bracketed by its own session_meta. Defaults to false.
+ IncludeSubagents bool `json:"include_subagents,omitempty" toon:"include_subagents,omitempty"`
+ // Session identifier to export.
+ SessionID string `json:"session_id,omitempty" toon:"session_id,omitempty"`
+}
+
+// SessionGetRequest is generated from the Flashduty OpenAPI schema.
+type SessionGetRequest struct {
+ // Alias for num_recent_events; takes precedence when both are set.
+ Limit int64 `json:"limit,omitempty" toon:"limit,omitempty"`
+ // Number of most-recent events to return; 0 uses the server default.
+ NumRecentEvents int64 `json:"num_recent_events,omitempty" toon:"num_recent_events,omitempty"`
+ // Opaque keyset cursor from a previous response's search_after_ctx, to page backward through older events.
+ SearchAfterCtx string `json:"search_after_ctx,omitempty" toon:"search_after_ctx,omitempty"`
+ // Session identifier.
+ SessionID string `json:"session_id,omitempty" toon:"session_id,omitempty"`
+}
+
+// SessionGetResponse is generated from the Flashduty OpenAPI schema.
+type SessionGetResponse struct {
+ // Recent events, ascending by (created_at, event_id).
+ Events []EventItem `json:"events" toon:"events"`
+ // True when older events remain beyond this page.
+ HasMoreOlder bool `json:"has_more_older" toon:"has_more_older"`
+ // Opaque keyset cursor; pass back as search_after_ctx to fetch the next older page. Omitted when has_more_older is false.
+ SearchAfterCtx string `json:"search_after_ctx" toon:"search_after_ctx"`
+ Session SessionItem `json:"session" toon:"session"`
+}
+
+// SessionItem is generated from the Flashduty OpenAPI schema.
+type SessionItem struct {
+ // Agent app that owns the session.
+ AppName string `json:"app_name" toon:"app_name"`
+ // Unix timestamp in milliseconds when archived; 0 means not archived.
+ ArchivedAt TimestampMilli `json:"archived_at" toon:"archived_at"`
+ BoundEnvironment EnvironmentBinding `json:"bound_environment" toon:"bound_environment"`
+ // True when the caller may rename/archive/delete the session.
+ CanManage bool `json:"can_manage" toon:"can_manage"`
+ ContextResolved ContextResolvedItem `json:"context_resolved" toon:"context_resolved"`
+ // The bound model's max context size in tokens. 0 means unknown.
+ ContextWindow int64 `json:"context_window" toon:"context_window"`
+ // Unix timestamp in milliseconds when the session was created.
+ CreatedAt TimestampMilli `json:"created_at" toon:"created_at"`
+ // Size in tokens of the LLM context window as of the most recent turn. 0 means no turn has completed.
+ CurrentContextTokens int64 `json:"current_context_tokens" toon:"current_context_tokens"`
+ // Surface that created the session.
+ EntryKind string `json:"entry_kind" toon:"entry_kind"`
+ // True when there is assistant output the caller has not yet viewed.
+ HasUnread bool `json:"has_unread" toon:"has_unread"`
+ // True for incognito (non-persisted-memory) sessions.
+ Incognito bool `json:"incognito" toon:"incognito"`
+ // True when the caller created this session.
+ IsMine bool `json:"is_mine" toon:"is_mine"`
+ // True when an agent turn is currently in flight for this session.
+ IsRunning bool `json:"is_running" toon:"is_running"`
+ // Unix timestamp in milliseconds of the most recent assistant-side event.
+ LastEventAt TimestampMilli `json:"last_event_at" toon:"last_event_at"`
+ // Parent session id for subagent (child) sessions; empty otherwise.
+ ParentSessionID string `json:"parent_session_id" toon:"parent_session_id"`
+ // Creator person id.
+ PersonID string `json:"person_id" toon:"person_id"`
+ // Caller's per-user pin timestamp in milliseconds; 0 means not pinned.
+ PinnedAt TimestampMilli `json:"pinned_at" toon:"pinned_at"`
+ // Session identifier.
+ SessionID string `json:"session_id" toon:"session_id"`
+ // Session title; may be empty for untitled sessions.
+ SessionName string `json:"session_name" toon:"session_name"`
+ // Raw session-state bag (session-scoped keys). Omitted when empty.
+ State map[string]any `json:"state" toon:"state"`
+ // Lifecycle status.
+ Status string `json:"status" toon:"status"`
+ // Owning team id; 0 means no team is bound. Immutable after create.
+ TeamID int64 `json:"team_id" toon:"team_id"`
+ // Resolved team name; empty for unbound rows or deleted teams.
+ TeamName string `json:"team_name" toon:"team_name"`
+ // Current save→validate round id (template-assistant only); empty otherwise.
+ TemplateStagingRoundID string `json:"template_staging_round_id" toon:"template_staging_round_id"`
+ TokenUsage SessionTokenUsage `json:"token_usage" toon:"token_usage"`
+ // Unix timestamp in milliseconds of the last session update.
+ UpdatedAt TimestampMilli `json:"updated_at" toon:"updated_at"`
+}
+
+// SessionListRequest is generated from the Flashduty OpenAPI schema.
+type SessionListRequest struct {
+ ListOptions
+ // Agent app whose sessions to list.
+ AppName string `json:"app_name,omitempty" toon:"app_name,omitempty"`
+ // Ascending sort when true; defaults to false (descending). Only honored when orderby is set.
+ Asc bool `json:"asc,omitempty" toon:"asc,omitempty"`
+ // Restrict to sessions produced by these entry surfaces. Empty returns every kind.
+ EntryKinds []string `json:"entry_kinds,omitempty" toon:"entry_kinds,omitempty"`
+ // Include subagent (child) sessions in the result; defaults to false.
+ IncludeSubagentSessions bool `json:"include_subagent_sessions,omitempty" toon:"include_subagent_sessions,omitempty"`
+ // Case-insensitive substring match against session name.
+ Keyword string `json:"keyword,omitempty" toon:"keyword,omitempty"`
+ // Sort column.
+ Orderby string `json:"orderby,omitempty" toon:"orderby,omitempty"`
+ // Visibility scope: all (own + member-of-team rows, the default), personal (own only), or team (member teams only).
+ Scope string `json:"scope,omitempty" toon:"scope,omitempty"`
+ // Archive bucket: active (default, not archived), archived, or all.
+ Status string `json:"status,omitempty" toon:"status,omitempty"`
+ // Optional explicit team filter; intersected with the caller's visible set / scope.
+ TeamIDs []int64 `json:"team_ids,omitempty" toon:"team_ids,omitempty"`
+}
+
+// SessionListResponse is generated from the Flashduty OpenAPI schema.
+type SessionListResponse struct {
+ // The page of sessions.
+ Sessions []SessionItem `json:"sessions" toon:"sessions"`
+ // Total number of sessions matching the filter (ignoring pagination).
+ Total int64 `json:"total" toon:"total"`
+}
+
+// SessionTokenUsage is generated from the Flashduty OpenAPI schema.
+type SessionTokenUsage struct {
+ // Portion of input_tokens served from the prompt cache.
+ CachedTokens int64 `json:"cached_tokens" toon:"cached_tokens"`
+ // Total prompt (input) tokens, including the cached portion.
+ InputTokens int64 `json:"input_tokens" toon:"input_tokens"`
+ // Total generated (output) tokens.
+ OutputTokens int64 `json:"output_tokens" toon:"output_tokens"`
+ // Total reasoning/thinking tokens.
+ ReasoningTokens int64 `json:"reasoning_tokens" toon:"reasoning_tokens"`
+}
+
// SilenceRuleItem is generated from the Flashduty OpenAPI schema.
type SilenceRuleItem struct {
AccountID int64 `json:"account_id" toon:"account_id"`
diff --git a/openapi/openapi.en.json b/openapi/openapi.en.json
index cc20716..bb3fea1 100644
--- a/openapi/openapi.en.json
+++ b/openapi/openapi.en.json
@@ -124,6 +124,10 @@
{
"name": "AI SRE/Skills",
"description": "AI SRE agent skill management."
+ },
+ {
+ "name": "AI SRE/Sessions",
+ "description": "AI SRE agent session history — list, inspect, and export transcripts."
}
],
"paths": {
@@ -22147,6 +22151,316 @@
}
}
}
+ },
+ "/safari/session/list": {
+ "post": {
+ "operationId": "session-read-list",
+ "summary": "List sessions",
+ "description": "List agent sessions visible to the caller within the resolved account, filtered by app, entry surface, archive status, and team scope, with pagination. Reads are scoped to the person the app_key resolves to.",
+ "tags": [
+ "AI SRE/Sessions"
+ ],
+ "security": [
+ {
+ "AppKeyAuth": []
+ }
+ ],
+ "x-mint": {
+ "content": "## Restrictions\n\n| Aspect | Value |\n| ------ | ----- |\n| Rate limits | **1,000 requests/minute**; **50 requests/second** per account |\n| Permissions | None — any valid `app_key` can call this operation |\n",
+ "href": "/en/api-reference/ai-sre/sessions/session-read-list",
+ "metadata": {
+ "sidebarTitle": "List sessions"
+ }
+ },
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/SessionListRequest"
+ },
+ "example": {
+ "app_name": "ai-sre",
+ "limit": 2,
+ "orderby": "updated_at",
+ "scope": "all"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/ResponseEnvelope"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/components/schemas/SessionListResponse"
+ }
+ }
+ }
+ ]
+ },
+ "example": {
+ "request_id": "01HK8XQE3Z7JM2NTFQ5YJ8P9R4",
+ "data": {
+ "total": 988,
+ "sessions": [
+ {
+ "session_id": "sess_f8oDvqiG64uur6sBNsTc4u",
+ "session_name": "Investigate cloud-assistant first heartbeat",
+ "app_name": "ai-sre",
+ "entry_kind": "web",
+ "person_id": "3790925372131",
+ "team_id": 0,
+ "is_mine": false,
+ "can_manage": true,
+ "status": "enabled",
+ "incognito": false,
+ "created_at": 1780367971228,
+ "updated_at": 1780367993457,
+ "token_usage": {
+ "input_tokens": 14948,
+ "cached_tokens": 11520,
+ "output_tokens": 888,
+ "reasoning_tokens": 351
+ },
+ "current_context_tokens": 14948,
+ "context_window": 0,
+ "archived_at": 0,
+ "pinned_at": 0,
+ "last_event_at": 1780367992649,
+ "is_running": false,
+ "has_unread": true
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "429": {
+ "$ref": "#/components/responses/TooManyRequests"
+ },
+ "500": {
+ "$ref": "#/components/responses/ServerError"
+ }
+ }
+ }
+ },
+ "/safari/session/get": {
+ "post": {
+ "operationId": "session-read-info",
+ "summary": "Get session detail",
+ "description": "Fetch one session plus a backward-paged window of its most recent events. Use search_after_ctx to page through older history.",
+ "tags": [
+ "AI SRE/Sessions"
+ ],
+ "security": [
+ {
+ "AppKeyAuth": []
+ }
+ ],
+ "x-mint": {
+ "content": "## Restrictions\n\n| Aspect | Value |\n| ------ | ----- |\n| Rate limits | **1,000 requests/minute**; **50 requests/second** per account |\n| Permissions | None — any valid `app_key` can call this operation |\n",
+ "href": "/en/api-reference/ai-sre/sessions/session-read-info",
+ "metadata": {
+ "sidebarTitle": "Get session detail"
+ }
+ },
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/SessionGetRequest"
+ },
+ "example": {
+ "session_id": "sess_f8oDvqiG64uur6sBNsTc4u",
+ "num_recent_events": 50
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/ResponseEnvelope"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/components/schemas/SessionGetResponse"
+ }
+ }
+ }
+ ]
+ },
+ "example": {
+ "request_id": "01HK8XQE3Z7JM2NTFQ5YJ8P9R4",
+ "data": {
+ "session": {
+ "session_id": "sess_f8oDvqiG64uur6sBNsTc4u",
+ "session_name": "Investigate cloud-assistant first heartbeat",
+ "app_name": "ai-sre",
+ "entry_kind": "web",
+ "person_id": "3790925372131",
+ "team_id": 0,
+ "is_mine": false,
+ "can_manage": true,
+ "status": "enabled",
+ "incognito": false,
+ "created_at": 1780367971228,
+ "updated_at": 1780367993457,
+ "token_usage": {
+ "input_tokens": 14948,
+ "cached_tokens": 11520,
+ "output_tokens": 888,
+ "reasoning_tokens": 351
+ },
+ "current_context_tokens": 14948,
+ "context_window": 0,
+ "archived_at": 0,
+ "pinned_at": 0,
+ "last_event_at": 1780367992649,
+ "is_running": false,
+ "has_unread": true
+ },
+ "events": [
+ {
+ "event_id": "evt_3aZQ9p",
+ "session_id": "sess_f8oDvqiG64uur6sBNsTc4u",
+ "author": "user",
+ "partial": false,
+ "turn_complete": false,
+ "status": "normal",
+ "created_at": 1780367971241
+ },
+ {
+ "event_id": "evt_7bWk2r",
+ "session_id": "sess_f8oDvqiG64uur6sBNsTc4u",
+ "author": "ai-sre",
+ "content": {
+ "role": "model",
+ "parts": [
+ {
+ "text": "..."
+ }
+ ]
+ },
+ "partial": false,
+ "turn_complete": true,
+ "status": "normal",
+ "created_at": 1780367992649
+ }
+ ],
+ "has_more_older": false
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
+ },
+ "429": {
+ "$ref": "#/components/responses/TooManyRequests"
+ },
+ "500": {
+ "$ref": "#/components/responses/ServerError"
+ }
+ }
+ }
+ },
+ "/safari/session/export": {
+ "post": {
+ "operationId": "session-read-export",
+ "summary": "Export session transcript",
+ "description": "Stream a session's full event transcript as NDJSON (application/x-ndjson), one JSON object per line. The first line is always a session_meta envelope; subsequent lines are session events (see the ExportLine schema). Parse the body line-by-line and write to a file — do NOT buffer the entire transcript into memory. When include_subagents is true, each subagent_dispatch line is followed by the child session's own stream.",
+ "tags": [
+ "AI SRE/Sessions"
+ ],
+ "security": [
+ {
+ "AppKeyAuth": []
+ }
+ ],
+ "x-mint": {
+ "content": "## Restrictions\n\n| Aspect | Value |\n| ------ | ----- |\n| Rate limits | **1,000 requests/minute**; **50 requests/second** per account |\n| Permissions | None — any valid `app_key` can call this operation |\n",
+ "href": "/en/api-reference/ai-sre/sessions/session-read-export",
+ "metadata": {
+ "sidebarTitle": "Export session transcript"
+ }
+ },
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/SessionExportRequest"
+ },
+ "example": {
+ "session_id": "sess_f8oDvqiG64uur6sBNsTc4u",
+ "include_subagents": false
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Streaming NDJSON (application/x-ndjson). One JSON object per line, terminated by a newline. The first line is always a `session_meta` envelope; subsequent lines are session events (see ExportLine).",
+ "content": {
+ "application/x-ndjson": {
+ "schema": {
+ "type": "string",
+ "description": "Newline-delimited JSON stream. Parse line-by-line; do not buffer the whole body. Each line decodes to an ExportLine object — see #/components/schemas/ExportLine."
+ },
+ "example": "{\"type\":\"session_meta\",\"session_id\":\"sess_f8oDvqiG64uur6sBNsTc4u\",\"account_id\":2451002751131,\"app_name\":\"ai-sre\",\"started_at\":\"2026-06-02T02:39:31.228Z\",\"ended_at\":\"2026-06-02T02:39:53.457Z\",\"model\":\"deepseek-v4-pro\"}\n{\"type\":\"user_message\",\"seq\":1,\"ts\":\"2026-06-02T02:39:31.241Z\"}\n{\"type\":\"user_message\",\"seq\":2,\"ts\":\"2026-06-02T02:39:31.245Z\",\"content\":\" ... \"}\n{\"type\":\"final_answer\",\"seq\":3,\"ts\":\"2026-06-02T02:39:53.457Z\",\"content\":\"## Conclusion: noise alert, not a real incident ...\"}\n"
+ }
+ }
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
+ },
+ "429": {
+ "$ref": "#/components/responses/TooManyRequests"
+ },
+ "500": {
+ "$ref": "#/components/responses/ServerError"
+ }
+ }
+ }
}
},
"components": {
@@ -40452,6 +40766,593 @@
"description": "Whether the section and its components are hidden from summary endpoints."
}
}
+ },
+ "SessionListRequest": {
+ "type": "object",
+ "description": "Filters for listing agent sessions. Reads are scoped to the account the app_key resolves to and to the resolved person's visible teams.",
+ "properties": {
+ "app_name": {
+ "type": "string",
+ "description": "Agent app whose sessions to list.",
+ "enum": [
+ "ask-ai",
+ "support",
+ "support-website",
+ "support-flashcat",
+ "ai-sre",
+ "template-assistant"
+ ]
+ },
+ "p": {
+ "type": "integer",
+ "description": "1-based page number; defaults to 1."
+ },
+ "limit": {
+ "type": "integer",
+ "description": "Page size, 1..100; defaults to 20.",
+ "minimum": 1,
+ "maximum": 100
+ },
+ "orderby": {
+ "type": "string",
+ "description": "Sort column.",
+ "enum": [
+ "created_at",
+ "updated_at"
+ ]
+ },
+ "asc": {
+ "type": "boolean",
+ "description": "Ascending sort when true; defaults to false (descending). Only honored when orderby is set."
+ },
+ "include_subagent_sessions": {
+ "type": "boolean",
+ "description": "Include subagent (child) sessions in the result; defaults to false."
+ },
+ "keyword": {
+ "type": "string",
+ "maxLength": 64,
+ "description": "Case-insensitive substring match against session name."
+ },
+ "scope": {
+ "type": "string",
+ "description": "Visibility scope: all (own + member-of-team rows, the default), personal (own only), or team (member teams only).",
+ "enum": [
+ "all",
+ "personal",
+ "team"
+ ]
+ },
+ "team_ids": {
+ "type": "array",
+ "items": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "description": "Optional explicit team filter; intersected with the caller's visible set / scope."
+ },
+ "entry_kinds": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "web",
+ "im",
+ "api",
+ "scheduled"
+ ]
+ },
+ "description": "Restrict to sessions produced by these entry surfaces. Empty returns every kind."
+ },
+ "status": {
+ "type": "string",
+ "description": "Archive bucket: active (default, not archived), archived, or all.",
+ "enum": [
+ "active",
+ "archived",
+ "all"
+ ]
+ }
+ },
+ "required": [
+ "app_name"
+ ]
+ },
+ "SessionTokenUsage": {
+ "type": "object",
+ "description": "Cumulative session-level token rollup across all turns. The account-billing source of truth.",
+ "properties": {
+ "input_tokens": {
+ "type": "integer",
+ "format": "int64",
+ "description": "Total prompt (input) tokens, including the cached portion."
+ },
+ "cached_tokens": {
+ "type": "integer",
+ "format": "int64",
+ "description": "Portion of input_tokens served from the prompt cache."
+ },
+ "output_tokens": {
+ "type": "integer",
+ "format": "int64",
+ "description": "Total generated (output) tokens."
+ },
+ "reasoning_tokens": {
+ "type": "integer",
+ "format": "int64",
+ "description": "Total reasoning/thinking tokens."
+ }
+ }
+ },
+ "EnvironmentBinding": {
+ "type": "object",
+ "description": "The runner or cloud sandbox the session is bound to. Null until the first message.",
+ "properties": {
+ "kind": {
+ "type": "string",
+ "description": "Environment kind (e.g. runner, sandbox)."
+ },
+ "id": {
+ "type": "string",
+ "description": "Environment identifier."
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable environment name."
+ },
+ "status": {
+ "type": "string",
+ "description": "Binding status."
+ }
+ }
+ },
+ "ContextResolvedItem": {
+ "type": "object",
+ "description": "Snapshot of the three-tier knowledge-pack resolution for this session.",
+ "properties": {
+ "account_pack_id": {
+ "type": "string",
+ "description": "Resolved account-scoped pack id."
+ },
+ "team_pack_id": {
+ "type": "string",
+ "description": "Resolved team-scoped pack id."
+ },
+ "incident_id": {
+ "type": "string",
+ "description": "Bound incident id, when war-room originated."
+ },
+ "resolved_at_ms": {
+ "type": "integer",
+ "format": "int64",
+ "description": "Unix timestamp in milliseconds when the packs were resolved."
+ },
+ "versions": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "integer"
+ },
+ "description": "Per-pack resolved version map."
+ }
+ }
+ },
+ "SessionItem": {
+ "type": "object",
+ "description": "One agent session row.",
+ "properties": {
+ "session_id": {
+ "type": "string",
+ "description": "Session identifier."
+ },
+ "parent_session_id": {
+ "type": "string",
+ "description": "Parent session id for subagent (child) sessions; empty otherwise."
+ },
+ "session_name": {
+ "type": "string",
+ "description": "Session title; may be empty for untitled sessions."
+ },
+ "app_name": {
+ "type": "string",
+ "description": "Agent app that owns the session."
+ },
+ "entry_kind": {
+ "type": "string",
+ "description": "Surface that created the session.",
+ "enum": [
+ "web",
+ "im",
+ "api",
+ "scheduled",
+ "subagent"
+ ]
+ },
+ "person_id": {
+ "type": "string",
+ "description": "Creator person id."
+ },
+ "team_id": {
+ "type": "integer",
+ "format": "int64",
+ "description": "Owning team id; 0 means no team is bound. Immutable after create."
+ },
+ "team_name": {
+ "type": "string",
+ "description": "Resolved team name; empty for unbound rows or deleted teams."
+ },
+ "is_mine": {
+ "type": "boolean",
+ "description": "True when the caller created this session."
+ },
+ "can_manage": {
+ "type": "boolean",
+ "description": "True when the caller may rename/archive/delete the session."
+ },
+ "status": {
+ "type": "string",
+ "description": "Lifecycle status.",
+ "enum": [
+ "enabled",
+ "deleted"
+ ]
+ },
+ "incognito": {
+ "type": "boolean",
+ "description": "True for incognito (non-persisted-memory) sessions."
+ },
+ "created_at": {
+ "type": "integer",
+ "format": "int64",
+ "description": "Unix timestamp in milliseconds when the session was created."
+ },
+ "updated_at": {
+ "type": "integer",
+ "format": "int64",
+ "description": "Unix timestamp in milliseconds of the last session update."
+ },
+ "template_staging_round_id": {
+ "type": "string",
+ "description": "Current save→validate round id (template-assistant only); empty otherwise."
+ },
+ "state": {
+ "type": "object",
+ "additionalProperties": true,
+ "description": "Raw session-state bag (session-scoped keys). Omitted when empty."
+ },
+ "bound_environment": {
+ "$ref": "#/components/schemas/EnvironmentBinding"
+ },
+ "context_resolved": {
+ "$ref": "#/components/schemas/ContextResolvedItem"
+ },
+ "token_usage": {
+ "$ref": "#/components/schemas/SessionTokenUsage"
+ },
+ "current_context_tokens": {
+ "type": "integer",
+ "format": "int64",
+ "description": "Size in tokens of the LLM context window as of the most recent turn. 0 means no turn has completed."
+ },
+ "context_window": {
+ "type": "integer",
+ "format": "int64",
+ "description": "The bound model's max context size in tokens. 0 means unknown."
+ },
+ "archived_at": {
+ "type": "integer",
+ "format": "int64",
+ "description": "Unix timestamp in milliseconds when archived; 0 means not archived."
+ },
+ "pinned_at": {
+ "type": "integer",
+ "format": "int64",
+ "description": "Caller's per-user pin timestamp in milliseconds; 0 means not pinned."
+ },
+ "last_event_at": {
+ "type": "integer",
+ "format": "int64",
+ "description": "Unix timestamp in milliseconds of the most recent assistant-side event."
+ },
+ "is_running": {
+ "type": "boolean",
+ "description": "True when an agent turn is currently in flight for this session."
+ },
+ "has_unread": {
+ "type": "boolean",
+ "description": "True when there is assistant output the caller has not yet viewed."
+ }
+ }
+ },
+ "SessionListResponse": {
+ "type": "object",
+ "description": "A page of agent sessions.",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "format": "int64",
+ "description": "Total number of sessions matching the filter (ignoring pagination)."
+ },
+ "sessions": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/SessionItem"
+ },
+ "description": "The page of sessions."
+ }
+ }
+ },
+ "SessionGetRequest": {
+ "type": "object",
+ "description": "Fetch one session plus a recent page of its events.",
+ "properties": {
+ "session_id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Session identifier."
+ },
+ "num_recent_events": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 1000,
+ "description": "Number of most-recent events to return; 0 uses the server default."
+ },
+ "limit": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 1000,
+ "description": "Alias for num_recent_events; takes precedence when both are set."
+ },
+ "search_after_ctx": {
+ "type": "string",
+ "maxLength": 4096,
+ "description": "Opaque keyset cursor from a previous response's search_after_ctx, to page backward through older events."
+ }
+ },
+ "required": [
+ "session_id"
+ ]
+ },
+ "EventItem": {
+ "type": "object",
+ "description": "One persisted session event. content/actions/usage_metadata carry the raw ADK envelope; treat them as opaque structured payloads.",
+ "properties": {
+ "event_id": {
+ "type": "string",
+ "description": "Event identifier."
+ },
+ "session_id": {
+ "type": "string",
+ "description": "Owning session id."
+ },
+ "invocation_id": {
+ "type": "string",
+ "description": "ADK invocation id grouping a turn."
+ },
+ "author": {
+ "type": "string",
+ "description": "Event author (e.g. user, the agent name)."
+ },
+ "branch": {
+ "type": "string",
+ "description": "ADK branch path for nested agents."
+ },
+ "content": {
+ "type": "object",
+ "additionalProperties": true,
+ "description": "ADK content envelope {role, parts:[...]}."
+ },
+ "actions": {
+ "type": "object",
+ "additionalProperties": true,
+ "description": "ADK actions envelope (state deltas, transfers, escalation)."
+ },
+ "usage_metadata": {
+ "type": "object",
+ "additionalProperties": true,
+ "description": "Per-turn token usage metadata."
+ },
+ "partial": {
+ "type": "boolean",
+ "description": "True for a streaming partial chunk."
+ },
+ "turn_complete": {
+ "type": "boolean",
+ "description": "True on the terminal event of a turn."
+ },
+ "error_code": {
+ "type": "string",
+ "description": "Error code when the event represents a failure."
+ },
+ "error_message": {
+ "type": "string",
+ "description": "Human-readable error message, when present."
+ },
+ "status": {
+ "type": "string",
+ "description": "Event status.",
+ "enum": [
+ "normal",
+ "compressed"
+ ]
+ },
+ "created_at": {
+ "type": "integer",
+ "format": "int64",
+ "description": "Unix timestamp in milliseconds when the event was written."
+ }
+ }
+ },
+ "SessionGetResponse": {
+ "type": "object",
+ "description": "A session plus a backward-paged window of its events.",
+ "properties": {
+ "session": {
+ "$ref": "#/components/schemas/SessionItem"
+ },
+ "events": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/EventItem"
+ },
+ "description": "Recent events, ascending by (created_at, event_id)."
+ },
+ "has_more_older": {
+ "type": "boolean",
+ "description": "True when older events remain beyond this page."
+ },
+ "search_after_ctx": {
+ "type": "string",
+ "description": "Opaque keyset cursor; pass back as search_after_ctx to fetch the next older page. Omitted when has_more_older is false."
+ }
+ }
+ },
+ "SessionExportRequest": {
+ "type": "object",
+ "description": "Export the full event transcript of one session as a streaming NDJSON body.",
+ "properties": {
+ "session_id": {
+ "type": "string",
+ "description": "Session identifier to export."
+ },
+ "include_subagents": {
+ "type": "boolean",
+ "description": "When true, each subagent_dispatch line is followed by the child session's full event stream, bracketed by its own session_meta. Defaults to false."
+ }
+ },
+ "required": [
+ "session_id"
+ ]
+ },
+ "ExportUsage": {
+ "type": "object",
+ "description": "Per-LLM-call token counts on an export line. Absent fields are 0.",
+ "properties": {
+ "input_tokens": {
+ "type": "integer",
+ "description": "Prompt (input) tokens for the call."
+ },
+ "output_tokens": {
+ "type": "integer",
+ "description": "Generated (output) tokens for the call."
+ },
+ "cache_read": {
+ "type": "integer",
+ "description": "Tokens served from the prompt cache."
+ },
+ "cache_creation": {
+ "type": "integer",
+ "description": "Tokens written to the prompt cache."
+ }
+ }
+ },
+ "ExportLine": {
+ "type": "object",
+ "description": "One line of the NDJSON export stream. The `type` field discriminates the line: session_meta (always first), user_message, llm_call, tool_call, subagent_dispatch, final_answer, agent_text, or error (emitted only if the stream is truncated mid-export). Fields are sparse — only those relevant to the line type are present.",
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Line discriminator.",
+ "enum": [
+ "session_meta",
+ "user_message",
+ "llm_call",
+ "tool_call",
+ "subagent_dispatch",
+ "final_answer",
+ "agent_text",
+ "error"
+ ]
+ },
+ "seq": {
+ "type": "integer",
+ "description": "1-based monotonic sequence within the session (absent on session_meta)."
+ },
+ "session_id": {
+ "type": "string",
+ "description": "Session id (on session_meta)."
+ },
+ "account_id": {
+ "type": "integer",
+ "format": "int64",
+ "description": "Account id (on session_meta)."
+ },
+ "app_name": {
+ "type": "string",
+ "description": "Agent app (on session_meta)."
+ },
+ "parent_session_id": {
+ "type": "string",
+ "description": "Parent session id for child sessions (on session_meta)."
+ },
+ "started_at": {
+ "type": "string",
+ "format": "date-time",
+ "description": "RFC3339 start timestamp (session_meta uses session.created_at)."
+ },
+ "ended_at": {
+ "type": "string",
+ "format": "date-time",
+ "description": "RFC3339 end timestamp; stamped on llm_call/tool_call/session_meta."
+ },
+ "model": {
+ "type": "string",
+ "description": "Chat model provider key; on session_meta and llm_call."
+ },
+ "content": {
+ "type": "string",
+ "description": "Text content of the line (messages, answers, errors)."
+ },
+ "ts": {
+ "type": "string",
+ "format": "date-time",
+ "description": "RFC3339 timestamp of the event."
+ },
+ "usage": {
+ "$ref": "#/components/schemas/ExportUsage"
+ },
+ "name": {
+ "type": "string",
+ "description": "Tool name (on tool_call)."
+ },
+ "input": {
+ "type": "object",
+ "additionalProperties": true,
+ "description": "Tool call input arguments (on tool_call)."
+ },
+ "output": {
+ "type": "string",
+ "description": "Tool call output (on tool_call response side)."
+ },
+ "status": {
+ "type": "string",
+ "description": "Tool result status, e.g. ok or error."
+ },
+ "duration_ms": {
+ "type": "integer",
+ "format": "int64",
+ "description": "Call duration in milliseconds."
+ },
+ "input_bytes": {
+ "type": "integer",
+ "description": "Byte size of the tool input."
+ },
+ "output_bytes": {
+ "type": "integer",
+ "description": "Byte size of the tool output."
+ },
+ "error": {
+ "type": "string",
+ "description": "Error detail when a call failed."
+ },
+ "agent_name": {
+ "type": "string",
+ "description": "Dispatched subagent name (on subagent_dispatch)."
+ },
+ "child_session_id": {
+ "type": "string",
+ "description": "Child session id created by the dispatch (on subagent_dispatch)."
+ }
+ }
}
}
}
diff --git a/openapi/openapi.zh.json b/openapi/openapi.zh.json
index 1ce7910..f01028e 100644
--- a/openapi/openapi.zh.json
+++ b/openapi/openapi.zh.json
@@ -124,6 +124,10 @@
{
"name": "AI SRE/技能",
"description": "AI SRE 智能体技能管理。"
+ },
+ {
+ "name": "AI SRE/会话",
+ "description": "AI SRE 智能体会话历史 —— 查询、查看与导出会话记录。"
}
],
"paths": {
@@ -22139,6 +22143,316 @@
}
}
}
+ },
+ "/safari/session/list": {
+ "post": {
+ "operationId": "session-read-list",
+ "summary": "查询会话列表",
+ "description": "分页查询调用者在所属账户内可见的智能体会话,可按应用、入口来源、归档状态与团队范围过滤。读取范围限定为 app_key 解析出的人员可见范围。",
+ "tags": [
+ "AI SRE/会话"
+ ],
+ "security": [
+ {
+ "AppKeyAuth": []
+ }
+ ],
+ "x-mint": {
+ "content": "## 使用限制\n\n| 项目 | 说明 |\n| ---- | ---- |\n| 频率限制 | 每个账户 **每分钟 1,000 次**;**每秒 50 次** |\n| 权限 | 无 —— 任意有效 `app_key` 均可调用 |\n",
+ "href": "/zh/api-reference/ai-sre/sessions/session-read-list",
+ "metadata": {
+ "sidebarTitle": "查询会话列表"
+ }
+ },
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/SessionListRequest"
+ },
+ "example": {
+ "app_name": "ai-sre",
+ "limit": 2,
+ "orderby": "updated_at",
+ "scope": "all"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/ResponseEnvelope"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/components/schemas/SessionListResponse"
+ }
+ }
+ }
+ ]
+ },
+ "example": {
+ "request_id": "01HK8XQE3Z7JM2NTFQ5YJ8P9R4",
+ "data": {
+ "total": 988,
+ "sessions": [
+ {
+ "session_id": "sess_f8oDvqiG64uur6sBNsTc4u",
+ "session_name": "Investigate cloud-assistant first heartbeat",
+ "app_name": "ai-sre",
+ "entry_kind": "web",
+ "person_id": "3790925372131",
+ "team_id": 0,
+ "is_mine": false,
+ "can_manage": true,
+ "status": "enabled",
+ "incognito": false,
+ "created_at": 1780367971228,
+ "updated_at": 1780367993457,
+ "token_usage": {
+ "input_tokens": 14948,
+ "cached_tokens": 11520,
+ "output_tokens": 888,
+ "reasoning_tokens": 351
+ },
+ "current_context_tokens": 14948,
+ "context_window": 0,
+ "archived_at": 0,
+ "pinned_at": 0,
+ "last_event_at": 1780367992649,
+ "is_running": false,
+ "has_unread": true
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "429": {
+ "$ref": "#/components/responses/TooManyRequests"
+ },
+ "500": {
+ "$ref": "#/components/responses/ServerError"
+ }
+ }
+ }
+ },
+ "/safari/session/get": {
+ "post": {
+ "operationId": "session-read-info",
+ "summary": "查看会话详情",
+ "description": "查看单个会话详情,并返回其最近事件的一页(向更早方向分页)。使用 search_after_ctx 翻阅更早的历史。",
+ "tags": [
+ "AI SRE/会话"
+ ],
+ "security": [
+ {
+ "AppKeyAuth": []
+ }
+ ],
+ "x-mint": {
+ "content": "## 使用限制\n\n| 项目 | 说明 |\n| ---- | ---- |\n| 频率限制 | 每个账户 **每分钟 1,000 次**;**每秒 50 次** |\n| 权限 | 无 —— 任意有效 `app_key` 均可调用 |\n",
+ "href": "/zh/api-reference/ai-sre/sessions/session-read-info",
+ "metadata": {
+ "sidebarTitle": "查看会话详情"
+ }
+ },
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/SessionGetRequest"
+ },
+ "example": {
+ "session_id": "sess_f8oDvqiG64uur6sBNsTc4u",
+ "num_recent_events": 50
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/ResponseEnvelope"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/components/schemas/SessionGetResponse"
+ }
+ }
+ }
+ ]
+ },
+ "example": {
+ "request_id": "01HK8XQE3Z7JM2NTFQ5YJ8P9R4",
+ "data": {
+ "session": {
+ "session_id": "sess_f8oDvqiG64uur6sBNsTc4u",
+ "session_name": "Investigate cloud-assistant first heartbeat",
+ "app_name": "ai-sre",
+ "entry_kind": "web",
+ "person_id": "3790925372131",
+ "team_id": 0,
+ "is_mine": false,
+ "can_manage": true,
+ "status": "enabled",
+ "incognito": false,
+ "created_at": 1780367971228,
+ "updated_at": 1780367993457,
+ "token_usage": {
+ "input_tokens": 14948,
+ "cached_tokens": 11520,
+ "output_tokens": 888,
+ "reasoning_tokens": 351
+ },
+ "current_context_tokens": 14948,
+ "context_window": 0,
+ "archived_at": 0,
+ "pinned_at": 0,
+ "last_event_at": 1780367992649,
+ "is_running": false,
+ "has_unread": true
+ },
+ "events": [
+ {
+ "event_id": "evt_3aZQ9p",
+ "session_id": "sess_f8oDvqiG64uur6sBNsTc4u",
+ "author": "user",
+ "partial": false,
+ "turn_complete": false,
+ "status": "normal",
+ "created_at": 1780367971241
+ },
+ {
+ "event_id": "evt_7bWk2r",
+ "session_id": "sess_f8oDvqiG64uur6sBNsTc4u",
+ "author": "ai-sre",
+ "content": {
+ "role": "model",
+ "parts": [
+ {
+ "text": "..."
+ }
+ ]
+ },
+ "partial": false,
+ "turn_complete": true,
+ "status": "normal",
+ "created_at": 1780367992649
+ }
+ ],
+ "has_more_older": false
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
+ },
+ "429": {
+ "$ref": "#/components/responses/TooManyRequests"
+ },
+ "500": {
+ "$ref": "#/components/responses/ServerError"
+ }
+ }
+ }
+ },
+ "/safari/session/export": {
+ "post": {
+ "operationId": "session-read-export",
+ "summary": "导出会话记录",
+ "description": "以 NDJSON(application/x-ndjson)流式导出会话的完整事件记录,每行一个 JSON 对象。第一行始终为 session_meta 信封,其后为会话事件(见 ExportLine)。请逐行解析并写入文件,切勿将整段记录全部读入内存。include_subagents 为 true 时,每条 subagent_dispatch 行后会跟随子会话自身的事件流。",
+ "tags": [
+ "AI SRE/会话"
+ ],
+ "security": [
+ {
+ "AppKeyAuth": []
+ }
+ ],
+ "x-mint": {
+ "content": "## 使用限制\n\n| 项目 | 说明 |\n| ---- | ---- |\n| 频率限制 | 每个账户 **每分钟 1,000 次**;**每秒 50 次** |\n| 权限 | 无 —— 任意有效 `app_key` 均可调用 |\n",
+ "href": "/zh/api-reference/ai-sre/sessions/session-read-export",
+ "metadata": {
+ "sidebarTitle": "导出会话记录"
+ }
+ },
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/SessionExportRequest"
+ },
+ "example": {
+ "session_id": "sess_f8oDvqiG64uur6sBNsTc4u",
+ "include_subagents": false
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Streaming NDJSON (application/x-ndjson). One JSON object per line, terminated by a newline. The first line is always a `session_meta` envelope; subsequent lines are session events (see ExportLine).",
+ "content": {
+ "application/x-ndjson": {
+ "schema": {
+ "type": "string",
+ "description": "Newline-delimited JSON stream. Parse line-by-line; do not buffer the whole body. Each line decodes to an ExportLine object — see #/components/schemas/ExportLine."
+ },
+ "example": "{\"type\":\"session_meta\",\"session_id\":\"sess_f8oDvqiG64uur6sBNsTc4u\",\"account_id\":2451002751131,\"app_name\":\"ai-sre\",\"started_at\":\"2026-06-02T02:39:31.228Z\",\"ended_at\":\"2026-06-02T02:39:53.457Z\",\"model\":\"deepseek-v4-pro\"}\n{\"type\":\"user_message\",\"seq\":1,\"ts\":\"2026-06-02T02:39:31.241Z\"}\n{\"type\":\"user_message\",\"seq\":2,\"ts\":\"2026-06-02T02:39:31.245Z\",\"content\":\" ... \"}\n{\"type\":\"final_answer\",\"seq\":3,\"ts\":\"2026-06-02T02:39:53.457Z\",\"content\":\"## Conclusion: noise alert, not a real incident ...\"}\n"
+ }
+ }
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
+ },
+ "429": {
+ "$ref": "#/components/responses/TooManyRequests"
+ },
+ "500": {
+ "$ref": "#/components/responses/ServerError"
+ }
+ }
+ }
}
},
"components": {
@@ -40443,6 +40757,593 @@
"description": "是否在汇总接口中隐藏该分组及其组件。"
}
}
+ },
+ "SessionListRequest": {
+ "type": "object",
+ "description": "Filters for listing agent sessions. Reads are scoped to the account the app_key resolves to and to the resolved person's visible teams.",
+ "properties": {
+ "app_name": {
+ "type": "string",
+ "description": "Agent app whose sessions to list.",
+ "enum": [
+ "ask-ai",
+ "support",
+ "support-website",
+ "support-flashcat",
+ "ai-sre",
+ "template-assistant"
+ ]
+ },
+ "p": {
+ "type": "integer",
+ "description": "1-based page number; defaults to 1."
+ },
+ "limit": {
+ "type": "integer",
+ "description": "Page size, 1..100; defaults to 20.",
+ "minimum": 1,
+ "maximum": 100
+ },
+ "orderby": {
+ "type": "string",
+ "description": "Sort column.",
+ "enum": [
+ "created_at",
+ "updated_at"
+ ]
+ },
+ "asc": {
+ "type": "boolean",
+ "description": "Ascending sort when true; defaults to false (descending). Only honored when orderby is set."
+ },
+ "include_subagent_sessions": {
+ "type": "boolean",
+ "description": "Include subagent (child) sessions in the result; defaults to false."
+ },
+ "keyword": {
+ "type": "string",
+ "maxLength": 64,
+ "description": "Case-insensitive substring match against session name."
+ },
+ "scope": {
+ "type": "string",
+ "description": "Visibility scope: all (own + member-of-team rows, the default), personal (own only), or team (member teams only).",
+ "enum": [
+ "all",
+ "personal",
+ "team"
+ ]
+ },
+ "team_ids": {
+ "type": "array",
+ "items": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "description": "Optional explicit team filter; intersected with the caller's visible set / scope."
+ },
+ "entry_kinds": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "web",
+ "im",
+ "api",
+ "scheduled"
+ ]
+ },
+ "description": "Restrict to sessions produced by these entry surfaces. Empty returns every kind."
+ },
+ "status": {
+ "type": "string",
+ "description": "Archive bucket: active (default, not archived), archived, or all.",
+ "enum": [
+ "active",
+ "archived",
+ "all"
+ ]
+ }
+ },
+ "required": [
+ "app_name"
+ ]
+ },
+ "SessionTokenUsage": {
+ "type": "object",
+ "description": "Cumulative session-level token rollup across all turns. The account-billing source of truth.",
+ "properties": {
+ "input_tokens": {
+ "type": "integer",
+ "format": "int64",
+ "description": "Total prompt (input) tokens, including the cached portion."
+ },
+ "cached_tokens": {
+ "type": "integer",
+ "format": "int64",
+ "description": "Portion of input_tokens served from the prompt cache."
+ },
+ "output_tokens": {
+ "type": "integer",
+ "format": "int64",
+ "description": "Total generated (output) tokens."
+ },
+ "reasoning_tokens": {
+ "type": "integer",
+ "format": "int64",
+ "description": "Total reasoning/thinking tokens."
+ }
+ }
+ },
+ "EnvironmentBinding": {
+ "type": "object",
+ "description": "The runner or cloud sandbox the session is bound to. Null until the first message.",
+ "properties": {
+ "kind": {
+ "type": "string",
+ "description": "Environment kind (e.g. runner, sandbox)."
+ },
+ "id": {
+ "type": "string",
+ "description": "Environment identifier."
+ },
+ "name": {
+ "type": "string",
+ "description": "Human-readable environment name."
+ },
+ "status": {
+ "type": "string",
+ "description": "Binding status."
+ }
+ }
+ },
+ "ContextResolvedItem": {
+ "type": "object",
+ "description": "Snapshot of the three-tier knowledge-pack resolution for this session.",
+ "properties": {
+ "account_pack_id": {
+ "type": "string",
+ "description": "Resolved account-scoped pack id."
+ },
+ "team_pack_id": {
+ "type": "string",
+ "description": "Resolved team-scoped pack id."
+ },
+ "incident_id": {
+ "type": "string",
+ "description": "Bound incident id, when war-room originated."
+ },
+ "resolved_at_ms": {
+ "type": "integer",
+ "format": "int64",
+ "description": "Unix timestamp in milliseconds when the packs were resolved."
+ },
+ "versions": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "integer"
+ },
+ "description": "Per-pack resolved version map."
+ }
+ }
+ },
+ "SessionItem": {
+ "type": "object",
+ "description": "One agent session row.",
+ "properties": {
+ "session_id": {
+ "type": "string",
+ "description": "Session identifier."
+ },
+ "parent_session_id": {
+ "type": "string",
+ "description": "Parent session id for subagent (child) sessions; empty otherwise."
+ },
+ "session_name": {
+ "type": "string",
+ "description": "Session title; may be empty for untitled sessions."
+ },
+ "app_name": {
+ "type": "string",
+ "description": "Agent app that owns the session."
+ },
+ "entry_kind": {
+ "type": "string",
+ "description": "Surface that created the session.",
+ "enum": [
+ "web",
+ "im",
+ "api",
+ "scheduled",
+ "subagent"
+ ]
+ },
+ "person_id": {
+ "type": "string",
+ "description": "Creator person id."
+ },
+ "team_id": {
+ "type": "integer",
+ "format": "int64",
+ "description": "Owning team id; 0 means no team is bound. Immutable after create."
+ },
+ "team_name": {
+ "type": "string",
+ "description": "Resolved team name; empty for unbound rows or deleted teams."
+ },
+ "is_mine": {
+ "type": "boolean",
+ "description": "True when the caller created this session."
+ },
+ "can_manage": {
+ "type": "boolean",
+ "description": "True when the caller may rename/archive/delete the session."
+ },
+ "status": {
+ "type": "string",
+ "description": "Lifecycle status.",
+ "enum": [
+ "enabled",
+ "deleted"
+ ]
+ },
+ "incognito": {
+ "type": "boolean",
+ "description": "True for incognito (non-persisted-memory) sessions."
+ },
+ "created_at": {
+ "type": "integer",
+ "format": "int64",
+ "description": "Unix timestamp in milliseconds when the session was created."
+ },
+ "updated_at": {
+ "type": "integer",
+ "format": "int64",
+ "description": "Unix timestamp in milliseconds of the last session update."
+ },
+ "template_staging_round_id": {
+ "type": "string",
+ "description": "Current save→validate round id (template-assistant only); empty otherwise."
+ },
+ "state": {
+ "type": "object",
+ "additionalProperties": true,
+ "description": "Raw session-state bag (session-scoped keys). Omitted when empty."
+ },
+ "bound_environment": {
+ "$ref": "#/components/schemas/EnvironmentBinding"
+ },
+ "context_resolved": {
+ "$ref": "#/components/schemas/ContextResolvedItem"
+ },
+ "token_usage": {
+ "$ref": "#/components/schemas/SessionTokenUsage"
+ },
+ "current_context_tokens": {
+ "type": "integer",
+ "format": "int64",
+ "description": "Size in tokens of the LLM context window as of the most recent turn. 0 means no turn has completed."
+ },
+ "context_window": {
+ "type": "integer",
+ "format": "int64",
+ "description": "The bound model's max context size in tokens. 0 means unknown."
+ },
+ "archived_at": {
+ "type": "integer",
+ "format": "int64",
+ "description": "Unix timestamp in milliseconds when archived; 0 means not archived."
+ },
+ "pinned_at": {
+ "type": "integer",
+ "format": "int64",
+ "description": "Caller's per-user pin timestamp in milliseconds; 0 means not pinned."
+ },
+ "last_event_at": {
+ "type": "integer",
+ "format": "int64",
+ "description": "Unix timestamp in milliseconds of the most recent assistant-side event."
+ },
+ "is_running": {
+ "type": "boolean",
+ "description": "True when an agent turn is currently in flight for this session."
+ },
+ "has_unread": {
+ "type": "boolean",
+ "description": "True when there is assistant output the caller has not yet viewed."
+ }
+ }
+ },
+ "SessionListResponse": {
+ "type": "object",
+ "description": "A page of agent sessions.",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "format": "int64",
+ "description": "Total number of sessions matching the filter (ignoring pagination)."
+ },
+ "sessions": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/SessionItem"
+ },
+ "description": "The page of sessions."
+ }
+ }
+ },
+ "SessionGetRequest": {
+ "type": "object",
+ "description": "Fetch one session plus a recent page of its events.",
+ "properties": {
+ "session_id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Session identifier."
+ },
+ "num_recent_events": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 1000,
+ "description": "Number of most-recent events to return; 0 uses the server default."
+ },
+ "limit": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 1000,
+ "description": "Alias for num_recent_events; takes precedence when both are set."
+ },
+ "search_after_ctx": {
+ "type": "string",
+ "maxLength": 4096,
+ "description": "Opaque keyset cursor from a previous response's search_after_ctx, to page backward through older events."
+ }
+ },
+ "required": [
+ "session_id"
+ ]
+ },
+ "EventItem": {
+ "type": "object",
+ "description": "One persisted session event. content/actions/usage_metadata carry the raw ADK envelope; treat them as opaque structured payloads.",
+ "properties": {
+ "event_id": {
+ "type": "string",
+ "description": "Event identifier."
+ },
+ "session_id": {
+ "type": "string",
+ "description": "Owning session id."
+ },
+ "invocation_id": {
+ "type": "string",
+ "description": "ADK invocation id grouping a turn."
+ },
+ "author": {
+ "type": "string",
+ "description": "Event author (e.g. user, the agent name)."
+ },
+ "branch": {
+ "type": "string",
+ "description": "ADK branch path for nested agents."
+ },
+ "content": {
+ "type": "object",
+ "additionalProperties": true,
+ "description": "ADK content envelope {role, parts:[...]}."
+ },
+ "actions": {
+ "type": "object",
+ "additionalProperties": true,
+ "description": "ADK actions envelope (state deltas, transfers, escalation)."
+ },
+ "usage_metadata": {
+ "type": "object",
+ "additionalProperties": true,
+ "description": "Per-turn token usage metadata."
+ },
+ "partial": {
+ "type": "boolean",
+ "description": "True for a streaming partial chunk."
+ },
+ "turn_complete": {
+ "type": "boolean",
+ "description": "True on the terminal event of a turn."
+ },
+ "error_code": {
+ "type": "string",
+ "description": "Error code when the event represents a failure."
+ },
+ "error_message": {
+ "type": "string",
+ "description": "Human-readable error message, when present."
+ },
+ "status": {
+ "type": "string",
+ "description": "Event status.",
+ "enum": [
+ "normal",
+ "compressed"
+ ]
+ },
+ "created_at": {
+ "type": "integer",
+ "format": "int64",
+ "description": "Unix timestamp in milliseconds when the event was written."
+ }
+ }
+ },
+ "SessionGetResponse": {
+ "type": "object",
+ "description": "A session plus a backward-paged window of its events.",
+ "properties": {
+ "session": {
+ "$ref": "#/components/schemas/SessionItem"
+ },
+ "events": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/EventItem"
+ },
+ "description": "Recent events, ascending by (created_at, event_id)."
+ },
+ "has_more_older": {
+ "type": "boolean",
+ "description": "True when older events remain beyond this page."
+ },
+ "search_after_ctx": {
+ "type": "string",
+ "description": "Opaque keyset cursor; pass back as search_after_ctx to fetch the next older page. Omitted when has_more_older is false."
+ }
+ }
+ },
+ "SessionExportRequest": {
+ "type": "object",
+ "description": "Export the full event transcript of one session as a streaming NDJSON body.",
+ "properties": {
+ "session_id": {
+ "type": "string",
+ "description": "Session identifier to export."
+ },
+ "include_subagents": {
+ "type": "boolean",
+ "description": "When true, each subagent_dispatch line is followed by the child session's full event stream, bracketed by its own session_meta. Defaults to false."
+ }
+ },
+ "required": [
+ "session_id"
+ ]
+ },
+ "ExportUsage": {
+ "type": "object",
+ "description": "Per-LLM-call token counts on an export line. Absent fields are 0.",
+ "properties": {
+ "input_tokens": {
+ "type": "integer",
+ "description": "Prompt (input) tokens for the call."
+ },
+ "output_tokens": {
+ "type": "integer",
+ "description": "Generated (output) tokens for the call."
+ },
+ "cache_read": {
+ "type": "integer",
+ "description": "Tokens served from the prompt cache."
+ },
+ "cache_creation": {
+ "type": "integer",
+ "description": "Tokens written to the prompt cache."
+ }
+ }
+ },
+ "ExportLine": {
+ "type": "object",
+ "description": "One line of the NDJSON export stream. The `type` field discriminates the line: session_meta (always first), user_message, llm_call, tool_call, subagent_dispatch, final_answer, agent_text, or error (emitted only if the stream is truncated mid-export). Fields are sparse — only those relevant to the line type are present.",
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Line discriminator.",
+ "enum": [
+ "session_meta",
+ "user_message",
+ "llm_call",
+ "tool_call",
+ "subagent_dispatch",
+ "final_answer",
+ "agent_text",
+ "error"
+ ]
+ },
+ "seq": {
+ "type": "integer",
+ "description": "1-based monotonic sequence within the session (absent on session_meta)."
+ },
+ "session_id": {
+ "type": "string",
+ "description": "Session id (on session_meta)."
+ },
+ "account_id": {
+ "type": "integer",
+ "format": "int64",
+ "description": "Account id (on session_meta)."
+ },
+ "app_name": {
+ "type": "string",
+ "description": "Agent app (on session_meta)."
+ },
+ "parent_session_id": {
+ "type": "string",
+ "description": "Parent session id for child sessions (on session_meta)."
+ },
+ "started_at": {
+ "type": "string",
+ "format": "date-time",
+ "description": "RFC3339 start timestamp (session_meta uses session.created_at)."
+ },
+ "ended_at": {
+ "type": "string",
+ "format": "date-time",
+ "description": "RFC3339 end timestamp; stamped on llm_call/tool_call/session_meta."
+ },
+ "model": {
+ "type": "string",
+ "description": "Chat model provider key; on session_meta and llm_call."
+ },
+ "content": {
+ "type": "string",
+ "description": "Text content of the line (messages, answers, errors)."
+ },
+ "ts": {
+ "type": "string",
+ "format": "date-time",
+ "description": "RFC3339 timestamp of the event."
+ },
+ "usage": {
+ "$ref": "#/components/schemas/ExportUsage"
+ },
+ "name": {
+ "type": "string",
+ "description": "Tool name (on tool_call)."
+ },
+ "input": {
+ "type": "object",
+ "additionalProperties": true,
+ "description": "Tool call input arguments (on tool_call)."
+ },
+ "output": {
+ "type": "string",
+ "description": "Tool call output (on tool_call response side)."
+ },
+ "status": {
+ "type": "string",
+ "description": "Tool result status, e.g. ok or error."
+ },
+ "duration_ms": {
+ "type": "integer",
+ "format": "int64",
+ "description": "Call duration in milliseconds."
+ },
+ "input_bytes": {
+ "type": "integer",
+ "description": "Byte size of the tool input."
+ },
+ "output_bytes": {
+ "type": "integer",
+ "description": "Byte size of the tool output."
+ },
+ "error": {
+ "type": "string",
+ "description": "Error detail when a call failed."
+ },
+ "agent_name": {
+ "type": "string",
+ "description": "Dispatched subagent name (on subagent_dispatch)."
+ },
+ "child_session_id": {
+ "type": "string",
+ "description": "Child session id created by the dispatch (on subagent_dispatch)."
+ }
+ }
}
}
}
diff --git a/roundtrip_gen_test.go b/roundtrip_gen_test.go
index fc1cbe2..fe2a78d 100644
--- a/roundtrip_gen_test.go
+++ b/roundtrip_gen_test.go
@@ -143,6 +143,8 @@ var exampleDataDecoders = map[string]func(json.RawMessage) error{
"POST /safari/mcp/server/get": func(d json.RawMessage) error { var v McpServerItem; return json.Unmarshal(d, &v) },
"POST /safari/mcp/server/list": func(d json.RawMessage) error { var v McpServerListResponse; return json.Unmarshal(d, &v) },
"POST /safari/mcp/server/update": func(d json.RawMessage) error { var v McpServerItem; return json.Unmarshal(d, &v) },
+ "POST /safari/session/get": func(d json.RawMessage) error { var v SessionGetResponse; return json.Unmarshal(d, &v) },
+ "POST /safari/session/list": func(d json.RawMessage) error { var v SessionListResponse; return json.Unmarshal(d, &v) },
"POST /safari/skill/delete": func(d json.RawMessage) error { var v any; return json.Unmarshal(d, &v) },
"POST /safari/skill/disable": func(d json.RawMessage) error { var v any; return json.Unmarshal(d, &v) },
"POST /safari/skill/download": func(d json.RawMessage) error { var v string; return json.Unmarshal(d, &v) },
diff --git a/services_gen.go b/services_gen.go
index 2864aa4..c5fbf39 100644
--- a/services_gen.go
+++ b/services_gen.go
@@ -10,6 +10,7 @@ type service struct{ client *Client }
type genServices struct {
A2aAgents *A2aAgentsService
McpServers *McpServersService
+ Sessions *SessionsService
Skills *SkillsService
AlertRules *AlertRulesService
DataSources *DataSourcesService
@@ -42,6 +43,7 @@ func (c *Client) initServices() {
c.common.client = c
c.A2aAgents = (*A2aAgentsService)(&c.common)
c.McpServers = (*McpServersService)(&c.common)
+ c.Sessions = (*SessionsService)(&c.common)
c.Skills = (*SkillsService)(&c.common)
c.AlertRules = (*AlertRulesService)(&c.common)
c.DataSources = (*DataSourcesService)(&c.common)
diff --git a/sessions.go b/sessions.go
new file mode 100644
index 0000000..f2d2c89
--- /dev/null
+++ b/sessions.go
@@ -0,0 +1,36 @@
+// Code generated by internal/cmd/gen; DO NOT EDIT.
+
+package flashduty
+
+import "context"
+
+// SessionsService handles the "AI SRE/Sessions" API resource.
+type SessionsService service
+
+// Get session detail.
+//
+// Fetch one session plus a backward-paged window of its most recent events. Use search_after_ctx to page through older history.
+//
+// API: POST /safari/session/get (session-read-info).
+func (s *SessionsService) Info(ctx context.Context, req *SessionGetRequest) (*SessionGetResponse, *Response, error) {
+ out := new(SessionGetResponse)
+ resp, err := s.client.do(ctx, "/safari/session/get", req, out)
+ if err != nil {
+ return nil, resp, err
+ }
+ return out, resp, nil
+}
+
+// List sessions.
+//
+// List agent sessions visible to the caller within the resolved account, filtered by app, entry surface, archive status, and team scope, with pagination. Reads are scoped to the person the app_key resolves to.
+//
+// API: POST /safari/session/list (session-read-list).
+func (s *SessionsService) List(ctx context.Context, req *SessionListRequest) (*SessionListResponse, *Response, error) {
+ out := new(SessionListResponse)
+ resp, err := s.client.do(ctx, "/safari/session/list", req, out)
+ if err != nil {
+ return nil, resp, err
+ }
+ return out, resp, nil
+}
diff --git a/sessions_export.go b/sessions_export.go
new file mode 100644
index 0000000..d87ff80
--- /dev/null
+++ b/sessions_export.go
@@ -0,0 +1,99 @@
+package flashduty
+
+import (
+ "bufio"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+)
+
+// Export streams a session's full event transcript as newline-delimited JSON
+// (application/x-ndjson). The first line is always a session_meta envelope;
+// subsequent lines are session events. When req.IncludeSubagents is true, each
+// subagent_dispatch line is followed by the child session's own stream.
+//
+// Unlike the generated typed endpoints, the success body is NOT a JSON envelope:
+// it is a potentially large line-delimited stream meant to be written straight to
+// a file. The returned io.ReadCloser is the live HTTP response body — the caller
+// owns it and MUST Close it (a deferred close is correct). Parse it line-by-line
+// (see NewExportScanner / DecodeExportLine); do not buffer the whole transcript
+// into memory.
+//
+// On any non-2xx status the body is the usual JSON error envelope: Export reads
+// and closes it and returns a typed error (*ErrorResponse, or *RateLimitError on
+// 429) with a nil ReadCloser, matching the generated endpoints.
+//
+// API: POST /safari/session/export (session-read-export).
+func (s *SessionsService) Export(ctx context.Context, req *SessionExportRequest) (io.ReadCloser, *Response, error) {
+ httpReq, err := s.client.newRequest(ctx, http.MethodPost, "/safari/session/export", req)
+ if err != nil {
+ return nil, nil, err
+ }
+ // The success body is a stream, not a JSON envelope; ask for NDJSON.
+ httpReq.Header.Set("Accept", "application/x-ndjson")
+
+ httpResp, err := s.client.client.Do(httpReq)
+ if err != nil {
+ return nil, nil, fmt.Errorf("flashduty: request to %s failed: %v", sanitizeURL(httpReq.URL), sanitizeError(err))
+ }
+
+ resp := &Response{Response: httpResp, RequestID: httpResp.Header.Get("Flashcat-Request-Id")}
+ resp.RateLimit = parseRateLimit(httpResp.Header)
+
+ if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
+ // Error responses are JSON envelopes, even on a streaming endpoint. Drain
+ // (bounded) and close the body, then surface a typed error.
+ defer func() { _ = httpResp.Body.Close() }()
+ raw, _ := io.ReadAll(io.LimitReader(httpResp.Body, maxResponseBodySize))
+ var env envelope
+ _ = json.Unmarshal(raw, &env)
+ if env.RequestID != "" {
+ resp.RequestID = env.RequestID
+ }
+ apiErr := &ErrorResponse{
+ Response: httpResp,
+ Code: env.Error.codeOr(""),
+ Message: env.Error.errMessageOr(string(raw)),
+ RequestID: resp.RequestID,
+ }
+ return nil, resp, asAPIError(apiErr, resp.RateLimit)
+ }
+
+ // 2xx: hand the live stream to the caller, who owns Close.
+ return httpResp.Body, resp, nil
+}
+
+// NewExportScanner wraps an export stream in a bufio.Scanner configured to read
+// one NDJSON line per Scan, with a buffer large enough for the wide event lines
+// (tool output, llm calls) that the transcript can contain. Each token is one raw
+// JSON line; decode it with DecodeExportLine or json.Unmarshal into ExportLine.
+//
+// rc, _, err := client.Sessions.Export(ctx, req)
+// if err != nil { return err }
+// defer rc.Close()
+// sc := flashduty.NewExportScanner(rc)
+// for sc.Scan() {
+// line, err := flashduty.DecodeExportLine(sc.Bytes())
+// // ... handle line, or write sc.Bytes() straight to a file ...
+// }
+// return sc.Err()
+func NewExportScanner(r io.Reader) *bufio.Scanner {
+ sc := bufio.NewScanner(r)
+ // A single transcript line (e.g. a large tool_call output) can exceed the
+ // 64KB default token limit; allow up to maxResponseBodySize per line.
+ sc.Buffer(make([]byte, 0, 64*1024), maxResponseBodySize)
+ return sc
+}
+
+// DecodeExportLine unmarshals one raw NDJSON export line into an ExportLine. Use
+// the Type field to discriminate (session_meta, user_message, llm_call,
+// tool_call, subagent_dispatch, final_answer, agent_text, error).
+func DecodeExportLine(line []byte) (*ExportLine, error) {
+ var l ExportLine
+ if err := json.Unmarshal(line, &l); err != nil {
+ return nil, fmt.Errorf("flashduty: decoding export line: %w", err)
+ }
+ return &l, nil
+}
diff --git a/sessions_export_test.go b/sessions_export_test.go
new file mode 100644
index 0000000..1d6cc94
--- /dev/null
+++ b/sessions_export_test.go
@@ -0,0 +1,130 @@
+package flashduty
+
+import (
+ "context"
+ "errors"
+ "io"
+ "net/http"
+ "strings"
+ "testing"
+ "time"
+)
+
+// ndjsonBody is a realistic export stream: a session_meta envelope first, then a
+// run of event lines. A trailing newline (as the real endpoint emits) must not
+// produce a spurious empty token.
+const ndjsonBody = `{"type":"session_meta","session_id":"sess_abc","account_id":2451002751131,"app_name":"ai-sre","model":"deepseek-v4-pro"}
+{"type":"user_message","seq":1,"ts":"2026-06-02T02:39:31.241Z","content":"hi"}
+{"type":"tool_call","seq":2,"ts":"2026-06-02T02:39:32.000Z","name":"shell","status":"ok","output_bytes":42}
+{"type":"final_answer","seq":3,"ts":"2026-06-02T02:39:53.457Z","content":"done"}
+`
+
+func TestExportStreamsNDJSONLineByLine(t *testing.T) {
+ c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
+ // Endpoint, app_key injection, and the NDJSON Accept override.
+ if r.URL.Path != "/safari/session/export" {
+ t.Errorf("path = %s", r.URL.Path)
+ }
+ if got := r.URL.Query().Get("app_key"); got != "KEY" {
+ t.Errorf("app_key = %q", got)
+ }
+ if acc := r.Header.Get("Accept"); acc != "application/x-ndjson" {
+ t.Errorf("Accept = %q, want application/x-ndjson", acc)
+ }
+ // Request body carries the typed request.
+ body, _ := io.ReadAll(r.Body)
+ if !strings.Contains(string(body), `"session_id":"sess_abc"`) {
+ t.Errorf("request body = %s", body)
+ }
+ w.Header().Set("Flashcat-Request-Id", "RIDX")
+ w.Header().Set("Content-Type", "application/x-ndjson")
+ _, _ = io.WriteString(w, ndjsonBody)
+ })
+
+ rc, resp, err := c.Sessions.Export(context.Background(), &SessionExportRequest{SessionID: "sess_abc"})
+ if err != nil {
+ t.Fatalf("Export error: %v", err)
+ }
+ if rc == nil {
+ t.Fatal("Export returned nil ReadCloser")
+ }
+ defer func() { _ = rc.Close() }()
+ if resp == nil || resp.RequestID != "RIDX" {
+ t.Fatalf("response meta = %+v", resp)
+ }
+
+ // Consume line-by-line via the helper scanner; assert each line decodes and
+ // that session_meta is first.
+ sc := NewExportScanner(rc)
+ var types []string
+ var firstSessionID string
+ for sc.Scan() {
+ line, derr := DecodeExportLine(sc.Bytes())
+ if derr != nil {
+ t.Fatalf("decode line %q: %v", sc.Bytes(), derr)
+ }
+ types = append(types, line.Type)
+ if line.Type == "session_meta" {
+ firstSessionID = line.SessionID
+ }
+ }
+ if err := sc.Err(); err != nil {
+ t.Fatalf("scanner error: %v", err)
+ }
+
+ want := []string{"session_meta", "user_message", "tool_call", "final_answer"}
+ if len(types) != len(want) {
+ t.Fatalf("got %d lines %v, want %d %v", len(types), types, len(want), want)
+ }
+ for i := range want {
+ if types[i] != want[i] {
+ t.Fatalf("line %d type = %q, want %q (all: %v)", i, types[i], want[i], types)
+ }
+ }
+ if types[0] != "session_meta" {
+ t.Fatalf("first line must be session_meta, got %q", types[0])
+ }
+ if firstSessionID != "sess_abc" {
+ t.Fatalf("session_meta.session_id = %q, want sess_abc", firstSessionID)
+ }
+}
+
+func TestExportMapsErrorEnvelopeOnNon2xx(t *testing.T) {
+ c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusForbidden)
+ _, _ = io.WriteString(w, `{"request_id":"RIDE","error":{"code":"access_denied","message":"no"}}`)
+ })
+ rc, resp, err := c.Sessions.Export(context.Background(), &SessionExportRequest{SessionID: "sess_x"})
+ if rc != nil {
+ t.Fatal("ReadCloser must be nil on error")
+ }
+ var apiErr *ErrorResponse
+ if !errors.As(err, &apiErr) {
+ t.Fatalf("expected *ErrorResponse, got %T: %v", err, err)
+ }
+ if apiErr.Code != "access_denied" || apiErr.RequestID != "RIDE" {
+ t.Fatalf("error not mapped: %+v", apiErr)
+ }
+ if resp == nil || resp.StatusCode != http.StatusForbidden {
+ t.Fatalf("response status = %+v", resp)
+ }
+}
+
+func TestExportReturnsRateLimitErrorOn429(t *testing.T) {
+ c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Retry-After", "30")
+ w.WriteHeader(http.StatusTooManyRequests)
+ _, _ = io.WriteString(w, `{"error":{"code":"rate_limited","message":"slow down"}}`)
+ })
+ rc, _, err := c.Sessions.Export(context.Background(), &SessionExportRequest{SessionID: "sess_x"})
+ if rc != nil {
+ t.Fatal("ReadCloser must be nil on 429")
+ }
+ var rl *RateLimitError
+ if !errors.As(err, &rl) {
+ t.Fatalf("expected *RateLimitError, got %T: %v", err, err)
+ }
+ if rl.RetryAfter != 30*time.Second {
+ t.Fatalf("RetryAfter = %s, want 30s", rl.RetryAfter)
+ }
+}