Skip to content

Commit 1b1b257

Browse files
authored
Merge pull request #129 from daily-co/pcc-580
feat: Add detailed session view with resource metrics
2 parents 71e9015 + 1718194 commit 1b1b257

File tree

3 files changed

+145
-0
lines changed

3 files changed

+145
-0
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ All notable changes to **Pipecat Cloud** will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.2.19] - 2026-01-27
9+
10+
### Added
11+
12+
- Added detailed session view with resource metrics. Use `pcc agent sessions <agent> --id <session-id>`
13+
to see CPU and memory usage with sparkline visualizations and percentile summaries.
14+
815
## [0.2.18] - 2026-01-06
916

1017
### Added

src/pipecatcloud/api.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,20 @@ async def _agent_sessions(self, agent_name: str, org: str) -> dict | None:
475475
def agent_sessions(self):
476476
return self.create_api_method(self._agent_sessions)
477477

478+
async def _agent_session(self, agent_name: str, session_id: str, org: str) -> dict | None:
479+
url = f"{self.construct_api_url('services_sessions_path').format(org=org, service=agent_name)}/{session_id}"
480+
return await self._base_request("GET", url, not_found_is_empty=True)
481+
482+
@property
483+
def agent_session(self):
484+
"""Get details for a specific session including resource metrics.
485+
Args:
486+
agent_name: Name of the agent
487+
session_id: ID of the session
488+
org: Organization ID
489+
"""
490+
return self.create_api_method(self._agent_session)
491+
478492
async def _agent_session_terminate(
479493
self, agent_name: str, session_id: str, org: str
480494
) -> dict | None:

src/pipecatcloud/cli/commands/agent.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,46 @@
3838
agent_cli = typer.Typer(name="agent", help="Agent management", no_args_is_help=True)
3939

4040

41+
def sparkline(values: list[int | float], max_width: int = 50) -> str:
42+
"""Generate Unicode sparkline from values, downsampling if needed."""
43+
if not values:
44+
return ""
45+
46+
# Downsample if too many values
47+
if len(values) > max_width:
48+
bucket_size = len(values) / max_width
49+
downsampled = []
50+
for i in range(max_width):
51+
start = int(i * bucket_size)
52+
end = int((i + 1) * bucket_size)
53+
bucket = values[start:end]
54+
downsampled.append(sum(bucket) / len(bucket) if bucket else 0)
55+
values = downsampled
56+
57+
blocks = "▁▂▃▄▅▆▇█"
58+
lo, hi = min(values), max(values)
59+
if hi == lo:
60+
return blocks[4] * len(values) # flat line
61+
scale = (hi - lo) / 7
62+
return "".join(blocks[min(7, int((v - lo) / scale))] for v in values)
63+
64+
65+
def format_bytes(b: int) -> str:
66+
"""Format bytes as human-readable string."""
67+
if b >= 1024 * 1024 * 1024:
68+
return f"{b / (1024 * 1024 * 1024):.1f}GB"
69+
if b >= 1024 * 1024:
70+
return f"{b / (1024 * 1024):.0f}MB"
71+
if b >= 1024:
72+
return f"{b / 1024:.0f}KB"
73+
return f"{b}B"
74+
75+
76+
def format_cpu(millicores: int) -> str:
77+
"""Format CPU millicores as human-readable string."""
78+
return f"{millicores / 1000:.2f} cores"
79+
80+
4181
# ----- Agent Commands -----
4282

4383

@@ -292,6 +332,90 @@ async def sessions(
292332
console.error("No target agent name provided")
293333
return typer.Exit(1)
294334

335+
# If session_id is specified, fetch single session with detailed metrics
336+
if session_id:
337+
with Live(
338+
console.status(f"[dim]Looking up session '{session_id}'[/dim]", spinner="dots")
339+
) as live:
340+
data, error = await API.agent_session(
341+
agent_name=agent_name, session_id=session_id, org=org, live=live
342+
)
343+
live.stop()
344+
345+
if error:
346+
return typer.Exit()
347+
348+
if not data:
349+
console.error(f"Session '{session_id}' not found")
350+
return typer.Exit()
351+
352+
# Display detailed session view
353+
session_duration = format_duration(data.get("createdAt"), data.get("endedAt")) or "N/A"
354+
status = data.get("completionStatus", "")
355+
if data.get("endedAt"):
356+
status_display = "[red]Error (500)[/red]" if status == "500" else "Complete"
357+
else:
358+
status_display = "[yellow]Active[/yellow]"
359+
360+
# Build session info panel
361+
info_lines = [
362+
f"[bold]Session ID:[/bold] {data['sessionId']}",
363+
f"[bold]Status:[/bold] {status_display}"
364+
+ (f" ({status})" if status and status != "500" else ""),
365+
f"[bold]Duration:[/bold] {session_duration}",
366+
f"[bold]Created:[/bold] {format_timestamp(data.get('createdAt'))}",
367+
f"[bold]Ended:[/bold] {format_timestamp(data.get('endedAt')) if data.get('endedAt') else '[dim]N/A[/dim]'}",
368+
f"[bold]Bot Start:[/bold] {data.get('botStartSeconds')}s"
369+
if data.get("botStartSeconds") is not None
370+
else "[bold]Bot Start:[/bold] [dim]N/A[/dim]",
371+
f"[bold]Cold Start:[/bold] {'[red]Yes[/red]' if data.get('coldStart') else 'No'}",
372+
]
373+
374+
# Add resource metrics if available
375+
metrics = data.get("resourceMetrics")
376+
if metrics:
377+
timeseries = metrics.get("timeseries", [])
378+
sample_count = metrics.get("sampleCount", 0)
379+
380+
# Calculate duration from timeseries
381+
if len(timeseries) >= 2:
382+
ts_duration = timeseries[-1].get("t", 0) - timeseries[0].get("t", 0)
383+
ts_duration_str = f"{ts_duration}s"
384+
else:
385+
ts_duration_str = "N/A"
386+
387+
info_lines.append("")
388+
info_lines.append(
389+
f"[bold]Resource Metrics[/bold] ({sample_count} samples over {ts_duration_str}):"
390+
)
391+
392+
# CPU sparkline and percentiles
393+
cpu_values = [s.get("c", 0) for s in timeseries]
394+
cpu_spark = sparkline(cpu_values) if cpu_values else ""
395+
cpu_p50 = metrics.get("cpuMillicoresP50", 0)
396+
cpu_p99 = metrics.get("cpuMillicoresP99", 0)
397+
info_lines.append(
398+
f" CPU: {cpu_spark} p50: {format_cpu(cpu_p50)} p99: {format_cpu(cpu_p99)}"
399+
)
400+
401+
# Memory sparkline and percentiles
402+
mem_values = [s.get("m", 0) for s in timeseries]
403+
mem_spark = sparkline(mem_values) if mem_values else ""
404+
mem_p50 = int(metrics.get("memoryBytesP50", 0))
405+
mem_p99 = int(metrics.get("memoryBytesP99", 0))
406+
info_lines.append(
407+
f" Memory: {mem_spark} p50: {format_bytes(mem_p50)} p99: {format_bytes(mem_p99)}"
408+
)
409+
410+
console.success(
411+
Panel(
412+
"\n".join(info_lines),
413+
title=f"Session details for agent [bold]{agent_name}[/bold] [dim]({org})[/dim]",
414+
title_align="left",
415+
),
416+
)
417+
return
418+
295419
with Live(
296420
console.status(f"[dim]Looking up agent with name '{agent_name}'[/dim]", spinner="dots")
297421
) as live:

0 commit comments

Comments
 (0)