Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions web/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ export type {
SkillItem,
AgentDeploy,
Session,
AgentSnapshot,
SessionEvent,
Turn,
SessionResult,
Destination,
Delivery,
SandboxWebhook,
Expand Down Expand Up @@ -736,6 +738,19 @@ export const getSessionEvents = (id: string, level?: string) =>
S.SessionEventListSchema,
).then((r) => r.data)

// Turns — the per-submission execution records behind a session (state, timing,
// usage, error). Read-only; powers the submission-health panel. Newest first.
export const getSessionTurns = (id: string) =>
apiFetch(
`/v3/sessions/${id}/turns`,
{},
S.SessionTurnListSchema,
).then((r) => r.data)

// The latest turn + its result event (if the turn produced one).
export const getSessionResult = (id: string) =>
apiFetch(`/v3/sessions/${id}/result`, {}, S.SessionResultSchema)

// Steer — post a user message into a session.
export const sendMessage = (
id: string,
Expand Down
81 changes: 73 additions & 8 deletions web/src/api/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -657,10 +657,14 @@ const credentials = [

const sessions = [
{
// A flue (CF-native) session — meters at the gateway, so its `usage` is empty here
// (spend renders "—"); the runtime badge is accented to set it apart from brain-box.
id: 'ses_a1b2c3',
status: 'running',
agent_id: 'agt_3kf9xz',
agent_snapshot: { runtime: 'flue', model: 'anthropic/claude-haiku-4-5' },
head: 24,
usage: {},
created_at: at(0, 1),
last_turn: { state: 'running' },
sandboxes: { brain: 'sbx_a1b2c3d4e5', hands: 'sbx_f6g7h8i9j0' },
Expand All @@ -669,31 +673,40 @@ const sessions = [
id: 'ses_d4e5f6',
status: 'awaiting_input',
agent_id: 'agt_3kf9xz',
agent_snapshot: { runtime: 'claude', model: 'anthropic/claude-sonnet-5' },
head: 12,
usage: { cost_usd: 0.0231, input_tokens: 8200, output_tokens: 640 },
created_at: at(0, 3),
last_turn: { state: 'ok', yield_reason: 'needs_input' },
},
{
id: 'ses_g7h8i9',
status: 'idle',
agent_id: 'agt_7mq2aa',
agent_snapshot: { runtime: 'pi', model: 'anthropic/claude-sonnet-5' },
head: 41,
// No cost reported → the spend column falls back to a token total.
usage: { input_tokens: 15000, output_tokens: 2200 },
created_at: at(1, 2),
last_turn: { state: 'ok', yield_reason: 'completed' },
},
{
id: 'ses_j1k2l3',
status: 'failed',
agent_id: 'agt_3kf9xz',
agent_snapshot: { runtime: 'codex', model: 'openai/gpt-5.3-codex' },
head: 8,
usage: { cost_usd: 0.11 },
created_at: at(2, 5),
last_turn: { state: 'error', yield_reason: 'error' },
},
{
id: 'ses_m4n5o6',
status: 'archived',
agent_id: 'agt_7mq2aa',
agent_snapshot: { runtime: 'claude', model: 'anthropic/claude-opus-4-8' },
head: 60,
usage: { cost_usd: 1.42, input_tokens: 320000, output_tokens: 18400 },
created_at: at(4, 1),
last_turn: { state: 'ok', yield_reason: 'completed' },
},
Expand Down Expand Up @@ -722,15 +735,39 @@ const sessionEvents = [
{
id: 'evt_3',
seq: 3,
type: 'agent.thinking',
level: 'progress',
actor: { type: 'agent', display: 'PR Reviewer' },
body: {
text: 'Fetch the PR head, then read the auth middleware diff before commenting.',
},
ts: at(0, 1),
},
{
id: 'evt_4',
seq: 4,
type: 'tool.call',
level: 'progress',
actor: { type: 'runtime' },
body: { tool: 'bash', input: 'git fetch origin pull/412/head' },
ts: at(0, 1),
},
{
id: 'evt_4',
seq: 4,
id: 'evt_5',
seq: 5,
type: 'exec.completed',
level: 'progress',
actor: { type: 'runtime' },
body: {
tool: 'bash',
summary: 'fetched pull/412/head → FETCH_HEAD',
duration_ms: 412,
},
ts: at(0, 1),
},
{
id: 'evt_6',
seq: 6,
type: 'agent.message',
level: 'user',
actor: { type: 'agent', display: 'PR Reviewer' },
Expand All @@ -740,8 +777,8 @@ const sessionEvents = [
ts: at(0, 1),
},
{
id: 'evt_5',
seq: 5,
id: 'evt_7',
seq: 7,
type: 'turn.completed',
level: 'user',
actor: { type: 'runtime' },
Expand All @@ -750,6 +787,29 @@ const sessionEvents = [
},
]

// Turns power the submission-health panel (GET /v3/sessions/:id/turns), newest first.
const sessionTurns = [
{
id: 'trn_2',
state: 'ok',
yield_reason: 'needs_input',
started_at: at(0, 1),
completed_at: at(0, 1),
active_seconds: 6.4,
usage: { cost_usd: 0.0121, input_tokens: 4200, output_tokens: 310 },
},
{
id: 'trn_1',
state: 'error',
yield_reason: 'error',
started_at: at(0, 2),
completed_at: at(0, 2),
active_seconds: 2.1,
usage: {},
error: { code: 'provision_infra', message: 'brain sandbox failed to start' },
},
]

const destinations = [
{
id: 'dst_1',
Expand All @@ -767,8 +827,8 @@ const deliveries = [
{
id: 'dlv_1',
destination: 'dst_1',
event_id: 'evt_5',
event_seq: 5,
event_id: 'evt_7',
event_seq: 7,
status: 'delivered',
attempts: 1,
last_attempt_at: at(0, 1),
Expand All @@ -778,8 +838,8 @@ const deliveries = [
{
id: 'dlv_2',
destination: 'dst_1',
event_id: 'evt_4',
event_seq: 4,
event_id: 'evt_6',
event_seq: 6,
status: 'failed',
attempts: 3,
last_attempt_at: at(0, 1),
Expand Down Expand Up @@ -876,6 +936,11 @@ const ROUTES: Array<[RegExp, Handler]> = [
[/^\/v3\/agents$/, () => ({ data: agents })],
[/^\/v3\/credentials$/, () => ({ data: credentials })],
[/^\/v3\/sessions\/[^/]+\/events/, () => ({ data: sessionEvents })],
[/^\/v3\/sessions\/[^/]+\/turns$/, () => ({ data: sessionTurns })],
[
/^\/v3\/sessions\/[^/]+\/result$/,
() => ({ last_turn: sessionTurns[0], result: sessionEvents[6] }),
],
[/^\/v3\/sessions\/[^/]+\/destinations$/, () => ({ data: destinations })],
[/^\/v3\/sessions\/[^/]+\/deliveries$/, () => ({ data: deliveries })],
[/^\/v3\/sessions\/[^/]+$/, () => sessions[0]],
Expand Down
36 changes: 33 additions & 3 deletions web/src/api/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,10 +487,23 @@ export const SlackManifestResponseSchema = z.object({
status: z.string(),
})

// The pinned effective agent tuple (design 009 §3.5) the session ran with. `runtime`
// is what distinguishes flue from the brain-box runtimes (claude/codex/pi) in read views.
export const AgentSnapshotSchema = z.object({
runtime: z.string().nullish(),
model: z.string().nullish(),
prompt_hash: z.string().nullish(),
revision: z.union([z.string(), z.number()]).nullish(),
agent_revision_number: z.number().nullish(),
digest: z.string().nullish(),
skill_bundle_digest: z.string().nullish(),
})

export const SessionSchema = z.object({
id: z.string(),
status: z.string(),
agent_id: z.string().nullable().optional(),
agent_snapshot: AgentSnapshotSchema.nullish(),
credential_id: z.string().nullable().optional(),
head: z.coerce.number().optional(), // current event seq; API returns it as a string ("0")
last_turn: record.nullish(),
Expand Down Expand Up @@ -525,7 +538,11 @@ export const SessionEventSchema = z.object({
level: z.string(),
actor: ActorSchema.optional(),
body: z.unknown().optional(),
content_ref: z.string().nullish(), // set when body spilled to blob storage
// Set together when the body spilled to blob storage (body > 32KB): the inline
// `body` is absent/partial, `content_ref` points at the blob, `body_bytes` is the size.
content_ref: z.string().nullish(),
body_truncated: z.boolean().nullish(),
body_bytes: z.number().nullish(),
refs: record.nullish(),
source: z.string().optional(),
turn_id: z.string().nullable().optional(),
Expand All @@ -544,8 +561,19 @@ export const TurnSchema = z.object({
attempt: z.number().optional(),
started_at: z.string().nullable().optional(),
completed_at: z.string().nullable().optional(),
usage: record.optional(),
error: z.string().nullable().optional(),
active_seconds: z.number().nullish(),
result_event_id: z.string().nullish(),
usage: record.nullish(),
error: z.unknown().nullish(), // server serializes the error as an opaque object, not a string
})
export const SessionTurnListSchema = z.object({
data: z.array(TurnSchema),
next_cursor: z.string().nullish(),
})
// GET /v3/sessions/:id/result → the latest turn + its result event (if any).
export const SessionResultSchema = z.object({
last_turn: TurnSchema.nullable(),
result: SessionEventSchema.nullable(),
})

export const DestinationSchema = z.object({
Expand Down Expand Up @@ -583,8 +611,10 @@ export type Credential = z.infer<typeof CredentialSchema>
export type SlackConnection = z.infer<typeof SlackConnectionSchema>
export type SlackManifestResponse = z.infer<typeof SlackManifestResponseSchema>
export type Session = z.infer<typeof SessionSchema>
export type AgentSnapshot = z.infer<typeof AgentSnapshotSchema>
export type SessionEvent = z.infer<typeof SessionEventSchema>
export type Turn = z.infer<typeof TurnSchema>
export type SessionResult = z.infer<typeof SessionResultSchema>
export type Destination = z.infer<typeof DestinationSchema>
export type Delivery = z.infer<typeof DeliverySchema>

Expand Down
36 changes: 36 additions & 0 deletions web/src/components/runtime-badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Bot, Cloud, type LucideIcon } from 'lucide-react'
import { runtimeLabel } from '@/lib/runtimes'
import { cn } from '@/lib/utils'

// Runtime is a category, not a health state — so it gets a quiet, neutral pill (not a
// status tone). `flue` (the CF-native durable path) carries a subtle accent + a distinct
// icon so it reads apart from the brain-box runtimes (claude/codex/pi) at a glance.
const ICON: Record<string, LucideIcon> = {
flue: Cloud,
}

export function RuntimeBadge({
runtime,
className,
}: {
runtime: string | null | undefined
className?: string
}) {
if (!runtime) return <span className="text-muted-foreground text-sm">—</span>
const Icon = ICON[runtime] ?? Bot
const isFlue = runtime === 'flue'
return (
<span
className={cn(
'inline-flex items-center gap-1.5 rounded-md px-2 py-0.5 text-xs font-medium',
isFlue
? 'bg-status-running-bg text-status-running'
: 'bg-secondary text-secondary-foreground',
className,
)}
>
<Icon className="size-3.5" aria-hidden />
{runtimeLabel(runtime)}
</span>
)
}
Loading