# Conflicts: # backend/main.py # backend/schemas.py # sidecar/sidecar.py # sidecar/test_claude_args.py
427 lines
17 KiB
Python
427 lines
17 KiB
Python
"""Conversations: list cards, the detail page's thread, subagents, memories
|
|
and artefacts as embedded in a conversation (``backend/conversations.py``)."""
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Literal, Optional
|
|
|
|
from .base import Schema
|
|
from .files import Pricing
|
|
|
|
__all__ = [
|
|
"ToolUsage",
|
|
"ConvNotification",
|
|
"WorktreeInfo",
|
|
"ForkedFrom",
|
|
"CronRef",
|
|
"AgentRunRef",
|
|
"ConvMeta",
|
|
"ConvSummary",
|
|
"Usage",
|
|
"SubagentRef",
|
|
"SubagentListItem",
|
|
"SubagentsResponse",
|
|
"RunningAgent",
|
|
"ParentConvRef",
|
|
"Task",
|
|
"ConversationSummary",
|
|
"ThreadItem",
|
|
"TurnUsage",
|
|
"MemorySummary",
|
|
"ArtefactItem",
|
|
"PlanSummary",
|
|
"ConversationDetail",
|
|
"ConversationsResponse",
|
|
]
|
|
|
|
|
|
class ToolUsage(Schema):
|
|
tokens: int
|
|
cost: float
|
|
output: int
|
|
|
|
|
|
class ConvNotification(Schema):
|
|
title: Optional[str] = None
|
|
body: Optional[str] = None
|
|
type: Optional[str] = None
|
|
url: Optional[str] = None
|
|
# An image shown in the push (notify-done `--image`): a full URL or a
|
|
# `/local/<file>` path served from Home Assistant's www/ dir.
|
|
image: Optional[str] = None
|
|
at: Optional[str] = None
|
|
# The French line the desk phone read aloud (notify-done --spoken), kept so
|
|
# the UI can replay the push's sound. Absent on pushes that weren't spoken.
|
|
spoken: Optional[str] = None
|
|
|
|
|
|
class WorktreeInfo(Schema):
|
|
name: str
|
|
dir: Optional[str] = None
|
|
createdAt: Optional[str] = None
|
|
removedAt: Optional[str] = None
|
|
|
|
|
|
class ForkedFrom(Schema):
|
|
"""Where a forked conversation branched off: the source conversation and
|
|
the user-message ordinal (``mi``) whose edit created the fork."""
|
|
id: str # source conversation id (<slug>/<sid>.jsonl)
|
|
sessionId: Optional[str] = None # source session id
|
|
mi: int # message ordinal the fork cut before
|
|
|
|
|
|
class CronRef(Schema):
|
|
"""The cron job that spawned a conversation (see cron.py) — rendered as a
|
|
tag on the card/detail linking back to Settings → Cron."""
|
|
id: str
|
|
name: Optional[str] = None
|
|
|
|
|
|
class AgentRunRef(Schema):
|
|
"""The agent definition a conversation was launched from by the agent
|
|
page's "Run now" — the manual sibling of :class:`CronRef`, rendered as a
|
|
tag linking back to ``/agents/<name>``."""
|
|
name: str
|
|
at: Optional[str] = None
|
|
|
|
|
|
class ConvMeta(Schema):
|
|
state: str
|
|
# Title the session chose for itself (`conv-meta title "…"`); the backend
|
|
# already prefers it over the parsed one on every card, so the UI reads
|
|
# `title` — this is here for the raw-sidecar endpoint.
|
|
title: Optional[str] = None
|
|
projects: list[str]
|
|
services: list[str]
|
|
worktrees: Optional[list[WorktreeInfo]] = None
|
|
committed: Optional[str] = None
|
|
pushed: Optional[str] = None
|
|
merged: Optional[str] = None
|
|
deployed: Optional[str] = None
|
|
notified: Optional[str] = None
|
|
notifications: list[ConvNotification]
|
|
archived: bool = False
|
|
# Agent harness the session runs on: "claude" | "pi" (None = pre-field).
|
|
harness: Optional[str] = None
|
|
# The Claude account the session runs on / bills: "personal" | "work"
|
|
# (backend/accounts.py). Always resolved server-side — spawn stamp, else
|
|
# the transcript's import source, else the cwd rule, else the default.
|
|
account: Optional[str] = None
|
|
# The paired remote worker (backend/workers.py) the session runs on — the
|
|
# one the hub launched it on, else the one its transcript was mirrored
|
|
# from. None = the host sidecar / a terminal on the lab.
|
|
worker: Optional[str] = None
|
|
# Whether the session runs with thinking on. False ⇒ it was spawned (or last
|
|
# resumed) with thinking disabled. None = pre-field, i.e. on.
|
|
thinking: Optional[bool] = None
|
|
# The claude `--effort` level the session was spawned (or last resumed) with:
|
|
# "low" | "medium" | "high" | "xhigh" | "max". Claude-only, and None when the
|
|
# run never picked one (the CLI's own default) — pi runs never set it.
|
|
effort: Optional[str] = None
|
|
# Set when this conversation was forked off another one (edit & resubmit).
|
|
forkedFrom: Optional[ForkedFrom] = None
|
|
# Set when a cron job spawned this conversation.
|
|
cron: Optional[CronRef] = None
|
|
# Set when the agent page's "Run now" spawned this conversation.
|
|
agentRun: Optional[AgentRunRef] = None
|
|
# When the `complete` skill's end-of-task pass ran (its reply signs off
|
|
# with COMPLETED, so the marker on the transcript's last message is the
|
|
# record). Rendered as the COMPLETED seal on the conversation card.
|
|
completedAt: Optional[str] = None
|
|
# The conversation's published summary, written by the `complete` skill.
|
|
summary: Optional["ConvSummary"] = None
|
|
|
|
|
|
class ConvSummary(Schema):
|
|
"""A conversation's published summary: the filled-in scaffold markdown (see
|
|
scaffold.py) plus when it was published."""
|
|
markdown: str
|
|
at: Optional[str] = None
|
|
|
|
|
|
class Usage(Schema):
|
|
input: int
|
|
output: int
|
|
cacheRead: int
|
|
cacheWriteTokens: int
|
|
cacheWriteUnits: int
|
|
|
|
|
|
class SubagentRef(Schema):
|
|
id: str
|
|
agentType: Optional[str] = None
|
|
description: Optional[str] = None
|
|
title: Optional[str] = None
|
|
model: Optional[str] = None
|
|
tokens: Optional[int] = None
|
|
cost: Optional[float] = None
|
|
messages: Optional[int] = None
|
|
# Lifecycle of the subagent itself: ``running`` while the parent's Task call
|
|
# is still open (no tool_result yet), else ``finished``.
|
|
state: Optional[str] = None
|
|
startedAt: Optional[str] = None
|
|
endedAt: Optional[str] = None
|
|
# How many agents this one spawned in turn (its whole subtree) — they are
|
|
# linked from *its* page, not listed here.
|
|
subagentCount: Optional[int] = None
|
|
|
|
|
|
class SubagentListItem(Schema):
|
|
"""A flat subagent (sidechain) ref with its parent ids — the graph view's
|
|
node source (the top-level conversation list hides these). Agents nest:
|
|
``parentId`` is the transcript holding the Task card that spawned it (a
|
|
sidechain itself for a nested agent), ``rootId`` the top-level
|
|
conversation, ``depth`` 1 for a direct child of the root."""
|
|
id: str
|
|
parentId: str
|
|
rootId: Optional[str] = None
|
|
depth: Optional[int] = None
|
|
agentType: Optional[str] = None
|
|
description: Optional[str] = None
|
|
title: Optional[str] = None
|
|
tokens: Optional[int] = None
|
|
cost: Optional[float] = None
|
|
state: Optional[str] = None
|
|
|
|
|
|
class SubagentsResponse(Schema):
|
|
subagents: list[SubagentListItem]
|
|
|
|
|
|
class RunningAgent(Schema):
|
|
"""A Task/Agent call that hasn't returned — i.e. a subagent still running."""
|
|
toolUseId: str
|
|
agentType: Optional[str] = None
|
|
description: Optional[str] = None
|
|
startedAt: Optional[str] = None
|
|
|
|
|
|
class ParentConvRef(Schema):
|
|
id: str
|
|
title: Optional[str] = None
|
|
toolUseId: Optional[str] = None
|
|
agentType: Optional[str] = None
|
|
description: Optional[str] = None
|
|
|
|
|
|
class Task(Schema):
|
|
"""One task from the conversation's task list (TaskCreate/TaskUpdate, or the
|
|
older TodoWrite). ``status`` is one of pending / in_progress / completed /
|
|
cancelled."""
|
|
id: str
|
|
subject: str
|
|
status: str
|
|
|
|
|
|
class ConversationSummary(Schema):
|
|
id: str
|
|
sessionId: Optional[str] = None
|
|
project: Optional[str] = None
|
|
cwd: Optional[str] = None
|
|
gitBranch: Optional[str] = None
|
|
model: str
|
|
# Every model that produced a turn, busiest first (a session can switch
|
|
# models mid-thread). Rendered as the conversation's model tags.
|
|
models: Optional[list[str]] = None
|
|
# Every `--effort` level the turns actually ran at, busiest first — the read
|
|
# side of the composer's effort select, mined from the transcript. Empty for
|
|
# pi runs and for transcripts predating the CLI flag.
|
|
efforts: Optional[list[str]] = None
|
|
title: str
|
|
userTurns: int
|
|
assistantTurns: int
|
|
messages: int
|
|
startedAt: Optional[str] = None
|
|
endedAt: Optional[str] = None
|
|
tokens: int
|
|
cost: float
|
|
usage: Optional[Usage] = None
|
|
byTool: Optional[dict[str, ToolUsage]] = None
|
|
# Second-level breakdown of each activity bucket: bucket -> sub-label ->
|
|
# usage. bash by program, read/edit by file extension, msg by prompt/replies,
|
|
# skill as "skills read", other tool buckets by tool name. Powers the
|
|
# click-to-drill-down on the conversations dashboard chart.
|
|
byToolSub: Optional[dict[str, dict[str, ToolUsage]]] = None
|
|
subagentCount: Optional[int] = None
|
|
subagentTokens: Optional[int] = None
|
|
subagentCost: Optional[float] = None
|
|
# Subagents still in flight (their Task call never came back). The list card
|
|
# shows one "running" chip per entry while the conversation itself is running.
|
|
runningAgents: Optional[list[RunningAgent]] = None
|
|
# A deploy command this conversation issued hasn't returned yet. The list
|
|
# card shows a "Deploying…" chip while the conversation itself is running,
|
|
# same gating as `runningAgents`.
|
|
deploying: Optional[bool] = None
|
|
# Current context size: the last assistant API call's full prompt+reply
|
|
# (input + cache read/write + output) — what the next request replays.
|
|
# `contextModel` is the model that made that call, so the UI can pick the
|
|
# right context-window size (see ModelInfo.contextWindow).
|
|
contextTokens: Optional[int] = None
|
|
contextModel: Optional[str] = None
|
|
# The session's first API call's input side, split in two: the injected
|
|
# context (system prompt, CLAUDE.md, memories, other <system-reminder>
|
|
# blocks) vs the user's actual first message. The API reports only the
|
|
# combined total, so the message share is estimated from its length
|
|
# (~4 chars/token) and the remainder is attributed to context.
|
|
firstContextTokens: Optional[int] = None
|
|
firstMessageTokens: Optional[int] = None
|
|
tasks: Optional[list[Task]] = None
|
|
# What the conversation *used*, busiest first — the tool-side counterpart of
|
|
# `meta.projects`/`meta.services`, and what the tag registry derives its
|
|
# `skill:`/`agent:` tags from. Skills are the `Skill(...)` calls mined from
|
|
# the transcript (a spawned subagent's calls count as the parent's, since
|
|
# the child is never listed on its own); agents are the run list the agents
|
|
# catalog counts, so a card can't disagree with the agent page.
|
|
skillsUsed: Optional[list[str]] = None
|
|
agentsUsed: Optional[list[str]] = None
|
|
meta: Optional[ConvMeta] = None
|
|
|
|
|
|
class ThreadItem(Schema):
|
|
role: Literal["user", "assistant", "tool"]
|
|
# "turn" is a content-less carrier: an API call that rendered nothing (a
|
|
# redacted-thinking turn) but was still billed. It exists so the cost chart
|
|
# sees every priced turn; the viewer never draws it.
|
|
kind: Literal[
|
|
"text", "thinking", "tool_use", "tool_result", "interrupted", "paused", "held",
|
|
"turn"]
|
|
name: Optional[str] = None
|
|
input: Optional[Any] = None
|
|
text: Optional[str] = None
|
|
isError: Optional[bool] = None
|
|
result: Optional[str] = None
|
|
out: Optional[int] = None
|
|
turnTokens: Optional[int] = None
|
|
turnCost: Optional[float] = None
|
|
# The cached-prompt share of this turn: tokens replayed from the prompt cache
|
|
# and what they cost (10% of the input rate). The rest of `turnTokens` /
|
|
# `turnCost` is fresh input, cache writes and output.
|
|
turnCacheTokens: Optional[int] = None
|
|
turnCacheCost: Optional[float] = None
|
|
# The activity bucket this turn's cost was charged to in the `byTool`
|
|
# breakdown (msg / read / bash / …), so the cost chart colours and totals a
|
|
# turn exactly as "cost by activity" does.
|
|
turnBucket: Optional[str] = None
|
|
# On the first API call only: the share booked to the "context" bucket
|
|
# (injected system prompt, CLAUDE.md, memories), split off from this turn's
|
|
# own activity just as the breakdown does.
|
|
turnContextCost: Optional[float] = None
|
|
turnContextTokens: Optional[int] = None
|
|
# Only on the user's *first* message: how much of the session's first API
|
|
# call was injected context (system prompt, CLAUDE.md, memories, …) vs the
|
|
# message's own text (length-estimated split of the API-reported total).
|
|
firstContextTokens: Optional[int] = None
|
|
firstMessageTokens: Optional[int] = None
|
|
mi: Optional[int] = None
|
|
ts: Optional[str] = None
|
|
# On an assistant text item the CLI wrote for a failed API call
|
|
# (``isApiErrorMessage``): why — ``auth`` | ``rate_limit`` |
|
|
# ``cli_outdated`` | ``overloaded`` | ``network`` | ``unknown``.
|
|
apiError: Optional[str] = None
|
|
toolUseId: Optional[str] = None
|
|
subagent: Optional[SubagentRef] = None
|
|
# On a Task/Agent card: whether the agent it spawned is still running. Set
|
|
# even before the subagent's transcript exists, so a just-spawned agent's
|
|
# card reads "running" straight away.
|
|
agentRunning: Optional[bool] = None
|
|
# Wall-clock duration of this step: for a tool call, how long it ran (call →
|
|
# its result); for a message/thinking block, the assistant API call's latency
|
|
# (previous transcript record → this one).
|
|
durationMs: Optional[int] = None
|
|
# The model that produced this assistant step.
|
|
model: Optional[str] = None
|
|
# Size of a tool call's result payload (before clipping).
|
|
resultChars: Optional[int] = None
|
|
resultLines: Optional[int] = None
|
|
# On a `Skill` tool card: the injected SKILL.md body (shown collapsible in the
|
|
# card) and a repo-relative path to it (a link into the editor), so a skill
|
|
# invocation renders as one self-contained action instead of a huge "You" bubble.
|
|
skillBody: Optional[str] = None
|
|
skillPath: Optional[str] = None
|
|
# On a deploy tool call (npm run deploy / deploy*.sh / zipgo deploy / the
|
|
# deploy-html skill): the URL it reported, scraped from the raw — unclipped —
|
|
# output. Drives the deploy preview widget and the "open the deploy" gizmo.
|
|
deployedUrl: Optional[str] = None
|
|
|
|
|
|
class TurnUsage(Usage):
|
|
model: Optional[str] = None
|
|
|
|
|
|
class MemorySummary(Schema):
|
|
slug: str
|
|
name: str
|
|
description: str
|
|
type: Optional[str] = None
|
|
originSessionId: Optional[str] = None
|
|
projects: list[str]
|
|
services: list[str]
|
|
links: list[str]
|
|
words: int
|
|
bytes: int
|
|
updatedAt: Optional[str] = None
|
|
|
|
|
|
class ArtefactItem(Schema):
|
|
"""One generated artefact (screenshot, render, export) found under a
|
|
project/service's ``.ai/artefacts/<date>/<sessionId>/`` folder."""
|
|
kind: str # "project" | "service"
|
|
slug: str
|
|
name: str
|
|
date: str # the YYYY-MM-DD folder it landed in
|
|
path: str # <date>/<sessionId>/<file>, for /api/artefact
|
|
url: str # ready-to-use /api/artefact serve URL
|
|
bytes: int
|
|
mtime: Optional[str] = None
|
|
video: Optional[bool] = None
|
|
|
|
|
|
# A plan's card, as embedded in ``ConversationDetail.plans``. It lives here
|
|
# rather than in ``plans.py`` because that module already imports ``ConvMeta``
|
|
# from this one — keeping the package free of import cycles.
|
|
class PlanSummary(Schema):
|
|
slug: str
|
|
title: str
|
|
status: str
|
|
created: Optional[str] = None
|
|
updatedAt: Optional[str] = None
|
|
sessionId: Optional[str] = None
|
|
conversationIds: list[str]
|
|
projects: list[str]
|
|
services: list[str]
|
|
estCost: Optional[float] = None
|
|
estTimeMinutes: Optional[int] = None
|
|
estTokens: Optional[int] = None
|
|
files: int
|
|
steps: int
|
|
questions: int = 0
|
|
|
|
|
|
class ConversationDetail(ConversationSummary):
|
|
usage: Usage
|
|
thread: list[ThreadItem]
|
|
turns: list[TurnUsage]
|
|
memoriesRead: Optional[list[MemorySummary]] = None
|
|
memoriesCreated: Optional[list[MemorySummary]] = None
|
|
# Direct children only — a nested agent is linked from its own parent's page.
|
|
subagents: Optional[list[SubagentRef]] = None
|
|
# On a subagent page: the direct parent (+ the Task card that spawned it)…
|
|
parentConversation: Optional[ParentConvRef] = None
|
|
# …and every ancestor, root first, direct parent last (the breadcrumb).
|
|
parentChain: Optional[list[ParentConvRef]] = None
|
|
spawnDepth: Optional[int] = None
|
|
# Plans (data/plans/*.md) whose planning session is this conversation — the
|
|
# `plan` skill stamps its sessionId/conversationIds, so we join them back.
|
|
plans: Optional[list[PlanSummary]] = None
|
|
# Generated artefacts collected from the conversation's projects/services
|
|
# (.ai/artefacts/<date>/<sessionId>/), oldest first.
|
|
artefacts: Optional[list[ArtefactItem]] = None
|
|
|
|
|
|
class ConversationsResponse(Schema):
|
|
conversations: list[ConversationSummary]
|
|
# Total conversations matching the filter (pre-pagination).
|
|
count: Optional[int] = None
|
|
# Cursor for the next page (`?before=`); None when this page ends the list.
|
|
nextBefore: Optional[str] = None
|
|
pricing: Pricing
|