{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "agent-run",
  "title": "Agent run",
  "description": "The shape of an agent run every AI view reads (agents, plan, tool calls, approvals, usage), a tiny store to feed it events from any backend, and the formats for time, tokens and cost.",
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/theme.json"
  ],
  "files": [
    {
      "path": "registry/ndeko/lib/agent-run.ts",
      "content": "export type SeriesSlot = 1 | 2 | 3 | 4 | 5;\n\nexport type RunStatus =\n  'idle' | 'running' | 'waiting' | 'done' | 'failed' | 'cancelled';\n\nexport type AgentStatus = 'idle' | 'running' | 'waiting' | 'done' | 'failed';\n\nexport type StepStatus = 'upcoming' | 'running' | 'done' | 'failed' | 'skipped';\n\nexport type CallStatus = 'queued' | 'running' | 'waiting' | 'done' | 'failed';\n\nexport type RiskLevel = 'low' | 'medium' | 'high';\n\nexport type ApprovalScope = 'once' | 'session' | 'always';\n\nexport type ApprovalStatus = 'pending' | 'approved' | 'denied';\n\nexport type RunAgent = {\n  id: string;\n  name: string;\n  series: SeriesSlot;\n  status: AgentStatus;\n  role?: string;\n  model?: string;\n  task?: string;\n  parentId?: string;\n  costUsd?: number;\n  startedAt?: number;\n  endedAt?: number;\n};\n\nexport type PlanStep = {\n  id: string;\n  title: string;\n  status: StepStatus;\n  detail?: string;\n  substeps?: PlanStep[];\n  startedAt?: number;\n  endedAt?: number;\n};\n\nexport type ToolCall = {\n  id: string;\n  tool: string;\n  label: string;\n  status: CallStatus;\n  agentId?: string;\n  parentId?: string;\n  input?: Record<string, unknown>;\n  output?: unknown;\n  error?: string;\n  startedAt?: number;\n  endedAt?: number;\n};\n\nexport type ApprovalRequest = {\n  id: string;\n  title: string;\n  risk: RiskLevel;\n  status: ApprovalStatus;\n  agentId?: string;\n  detail?: string;\n  command?: string;\n  scope?: ApprovalScope;\n  requestedAt?: number;\n  resolvedAt?: number;\n};\n\nexport type RunUsage = {\n  inputTokens: number;\n  outputTokens: number;\n  costUsd: number;\n  contextTokens: number;\n  contextLimit: number;\n};\n\nexport type RunSnapshot = {\n  id: string;\n  title: string;\n  status: RunStatus;\n  agents: RunAgent[];\n  plan: PlanStep[];\n  calls: ToolCall[];\n  approvals: ApprovalRequest[];\n  usage: RunUsage;\n  startedAt?: number;\n  endedAt?: number;\n};\n\nexport type RunEvent =\n  | { type: 'run.started'; title?: string }\n  | { type: 'run.finished'; status: 'done' | 'failed' | 'cancelled' }\n  | { type: 'agent.added'; agent: RunAgent }\n  | { type: 'agent.updated'; id: string; patch: Partial<RunAgent> }\n  | { type: 'plan.set'; steps: PlanStep[] }\n  | { type: 'step.updated'; id: string; patch: Partial<PlanStep> }\n  | { type: 'call.started'; call: ToolCall }\n  | { type: 'call.updated'; id: string; patch: Partial<ToolCall> }\n  | { type: 'approval.requested'; approval: ApprovalRequest }\n  | {\n      type: 'approval.resolved';\n      id: string;\n      decision: 'approved' | 'denied';\n      scope?: ApprovalScope;\n    }\n  | {\n      type: 'usage.added';\n      inputTokens?: number;\n      outputTokens?: number;\n      costUsd?: number;\n      contextTokens?: number;\n    }\n  | { type: 'reset'; snapshot: RunSnapshot };\n\nexport type RunStore = {\n  getSnapshot: () => RunSnapshot;\n  subscribe: (listener: () => void) => () => void;\n  dispatch: (event: RunEvent) => void;\n};\n\nconst EMPTY_USAGE: RunUsage = {\n  inputTokens: 0,\n  outputTokens: 0,\n  costUsd: 0,\n  contextTokens: 0,\n  contextLimit: 200_000,\n};\n\nexport function emptyRun(partial: Partial<RunSnapshot> = {}): RunSnapshot {\n  return {\n    id: 'run',\n    title: 'Run',\n    status: 'idle',\n    agents: [],\n    plan: [],\n    calls: [],\n    approvals: [],\n    usage: EMPTY_USAGE,\n    ...partial,\n  };\n}\n\n/** The CSS colour of a series slot, for dots, lanes, lines and `--chip`. */\nexport function seriesColorOf(slot: SeriesSlot) {\n  return `var(--series-${slot})`;\n}\n\nfunction isFinishedStatus(status: string) {\n  const isDone = status === 'done';\n  const isFailed = status === 'failed';\n  const isSkipped = status === 'skipped';\n  return isDone || isFailed || isSkipped;\n}\n\nfunction stampedTimes<T extends { startedAt?: number; endedAt?: number }>(\n  patch: Partial<T> & { status?: string },\n  now: number\n) {\n  const stamped = { ...patch };\n  const isStarting = patch.status === 'running';\n  const needsStart = isStarting && patch.startedAt === undefined;\n  if (needsStart) {\n    stamped.startedAt = now;\n  }\n  const isEnding = isFinishedStatus(patch.status ?? '');\n  const needsEnd = isEnding && patch.endedAt === undefined;\n  if (needsEnd) {\n    stamped.endedAt = now;\n  }\n  return stamped;\n}\n\nfunction updatedSteps(\n  steps: PlanStep[],\n  id: string,\n  patch: Partial<PlanStep>\n): PlanStep[] {\n  return steps.map((step) => {\n    if (step.id === id) {\n      return { ...step, ...patch };\n    }\n    if (!step.substeps) {\n      return step;\n    }\n    return { ...step, substeps: updatedSteps(step.substeps, id, patch) };\n  });\n}\n\nfunction plus(total: number, amount: number | undefined) {\n  const added = amount ?? 0;\n  return total + added;\n}\n\nfunction hasPendingApproval(approvals: ApprovalRequest[]) {\n  return approvals.some((approval) => approval.status === 'pending');\n}\n\nfunction statusAfterApproval(\n  snapshot: RunSnapshot,\n  approvals: ApprovalRequest[]\n) {\n  const isWaiting = snapshot.status === 'waiting';\n  const stillWaiting = hasPendingApproval(approvals);\n  if (!isWaiting) {\n    return snapshot.status;\n  }\n  if (stillWaiting) {\n    return 'waiting';\n  }\n  return 'running';\n}\n\n/** Applies one event to a run. Times the event leaves out are stamped with `now`. */\nexport function reduceRun(\n  snapshot: RunSnapshot,\n  event: RunEvent,\n  now: number = Date.now()\n): RunSnapshot {\n  switch (event.type) {\n    case 'reset':\n      return event.snapshot;\n    case 'run.started': {\n      const title = event.title ?? snapshot.title;\n      return { ...snapshot, title, status: 'running', startedAt: now };\n    }\n    case 'run.finished':\n      return { ...snapshot, status: event.status, endedAt: now };\n    case 'agent.added':\n      return { ...snapshot, agents: [...snapshot.agents, event.agent] };\n    case 'agent.updated': {\n      const patch = stampedTimes<RunAgent>(event.patch, now);\n      const agents = snapshot.agents.map((agent) => {\n        if (agent.id !== event.id) {\n          return agent;\n        }\n        return { ...agent, ...patch };\n      });\n      return { ...snapshot, agents };\n    }\n    case 'plan.set':\n      return { ...snapshot, plan: event.steps };\n    case 'step.updated': {\n      const patch = stampedTimes<PlanStep>(event.patch, now);\n      const plan = updatedSteps(snapshot.plan, event.id, patch);\n      return { ...snapshot, plan };\n    }\n    case 'call.started': {\n      const call = stampedTimes<ToolCall>(event.call, now) as ToolCall;\n      return { ...snapshot, calls: [...snapshot.calls, call] };\n    }\n    case 'call.updated': {\n      const patch = stampedTimes<ToolCall>(event.patch, now);\n      const calls = snapshot.calls.map((call) => {\n        if (call.id !== event.id) {\n          return call;\n        }\n        return { ...call, ...patch };\n      });\n      return { ...snapshot, calls };\n    }\n    case 'approval.requested': {\n      const approval = { requestedAt: now, ...event.approval };\n      const approvals = [...snapshot.approvals, approval];\n      const isRunning = snapshot.status === 'running';\n      if (isRunning) {\n        return { ...snapshot, approvals, status: 'waiting' };\n      }\n      return { ...snapshot, approvals };\n    }\n    case 'approval.resolved': {\n      const approvals = snapshot.approvals.map((approval) => {\n        if (approval.id !== event.id) {\n          return approval;\n        }\n        return {\n          ...approval,\n          status: event.decision,\n          scope: event.scope ?? approval.scope,\n          resolvedAt: now,\n        };\n      });\n      const status = statusAfterApproval(snapshot, approvals);\n      return { ...snapshot, approvals, status };\n    }\n    case 'usage.added': {\n      const usage = snapshot.usage;\n      const contextTokens = plus(usage.contextTokens, event.contextTokens);\n      const cappedContext = Math.min(contextTokens, usage.contextLimit);\n      const nextUsage: RunUsage = {\n        ...usage,\n        inputTokens: plus(usage.inputTokens, event.inputTokens),\n        outputTokens: plus(usage.outputTokens, event.outputTokens),\n        costUsd: plus(usage.costUsd, event.costUsd),\n        contextTokens: cappedContext,\n      };\n      return { ...snapshot, usage: nextUsage };\n    }\n  }\n}\n\n/** A tiny store every agent view can read: feed it events from any backend. */\nexport function createRunStore(initial: RunSnapshot): RunStore {\n  let snapshot = initial;\n  const listeners = new Set<() => void>();\n\n  function notify() {\n    for (const listener of listeners) {\n      listener();\n    }\n  }\n\n  return {\n    getSnapshot() {\n      return snapshot;\n    },\n    subscribe(listener) {\n      listeners.add(listener);\n      return () => {\n        listeners.delete(listener);\n      };\n    },\n    dispatch(event) {\n      snapshot = reduceRun(snapshot, event);\n      notify();\n    },\n  };\n}\n\nexport function pendingApprovalsOf(snapshot: RunSnapshot) {\n  return snapshot.approvals.filter((approval) => approval.status === 'pending');\n}\n\nexport function elapsedOf(\n  item: { startedAt?: number; endedAt?: number },\n  now: number\n) {\n  if (item.startedAt === undefined) {\n    return 0;\n  }\n  const end = item.endedAt ?? now;\n  return Math.max(0, end - item.startedAt);\n}\n\n/** 0.4s, 4.2s, 42s, 1m 04s, 1h 02m. */\nexport function formatDuration(milliseconds: number) {\n  const seconds = milliseconds / 1000;\n  if (seconds < 10) {\n    return `${seconds.toFixed(1)}s`;\n  }\n  if (seconds < 60) {\n    return `${Math.floor(seconds)}s`;\n  }\n  const minutes = Math.floor(seconds / 60);\n  if (minutes < 60) {\n    const restSeconds = Math.floor(seconds % 60);\n    const paddedSeconds = String(restSeconds).padStart(2, '0');\n    return `${minutes}m ${paddedSeconds}s`;\n  }\n  const hours = Math.floor(minutes / 60);\n  const restMinutes = minutes % 60;\n  const paddedMinutes = String(restMinutes).padStart(2, '0');\n  return `${hours}h ${paddedMinutes}m`;\n}\n\n/** 940, 4.2k, 42k, 1.2M. */\nexport function formatTokens(count: number) {\n  if (count < 1000) {\n    return String(Math.round(count));\n  }\n  const thousands = count / 1000;\n  if (count < 10_000) {\n    return `${thousands.toFixed(1)}k`;\n  }\n  if (count < 1_000_000) {\n    return `${Math.round(thousands)}k`;\n  }\n  const millions = count / 1_000_000;\n  return `${millions.toFixed(1)}M`;\n}\n\nconst SMALL_COST = new Intl.NumberFormat('en-US', {\n  style: 'currency',\n  currency: 'USD',\n  minimumFractionDigits: 3,\n  maximumFractionDigits: 3,\n});\n\nconst LARGE_COST = new Intl.NumberFormat('en-US', {\n  style: 'currency',\n  currency: 'USD',\n  minimumFractionDigits: 2,\n  maximumFractionDigits: 2,\n});\n\n/** $0.042 under a dollar, $12.40 above. */\nexport function formatCost(usd: number) {\n  const isSmall = usd < 1;\n  if (isSmall) {\n    return SMALL_COST.format(usd);\n  }\n  return LARGE_COST.format(usd);\n}\n",
      "type": "registry:lib"
    }
  ],
  "categories": [
    "foundations"
  ],
  "type": "registry:lib"
}