{
  "schemaVersion": 1,
  "target": "react",
  "name": "agent-run",
  "version": "0.1.1",
  "category": "agent",
  "title": "Agent Run State",
  "description": "Types for runs, steps, approvals, messages, and artifacts, plus an event reducer that de-duplicates, reorders, and recovers from snapshots.",
  "tags": [
    "agent",
    "events",
    "state",
    "stream",
    "reducer"
  ],
  "requires": {
    "projectTypes": [
      "next",
      "vite-react"
    ],
    "styling": "tailwind-v4"
  },
  "npmDependencies": {},
  "registryDependencies": [],
  "docsPath": "/components/agent-run",
  "props": [
    {
      "name": "fromSnapshot(run, sequence?)",
      "type": "(AgentRun, number) => RunStore",
      "description": "Starts a store from a run snapshot from your API and the event sequence it reflects."
    },
    {
      "name": "applyEvents(store, events)",
      "type": "(RunStore, AgentRunEvent[]) => RunStore",
      "description": "Applies events: ignores duplicate ids and other runs, buffers early events, and applies them in sequence order. Pure."
    },
    {
      "name": "hasGap(store)",
      "type": "(RunStore) => boolean",
      "description": "True when events are waiting behind a missing sequence. Refresh from a snapshot if the gap does not fill."
    },
    {
      "name": "AgentRunEvent",
      "type": "{ id, runId, sequence, timestamp, type, payload }",
      "description": "Event contract: run.status, step.started, step.updated, approval.requested, approval.resolved, message.started, message.delta, message.completed, artifact.updated."
    }
  ],
  "accessibility": [
    "No UI. The agent components that render this state announce status changes with live regions."
  ],
  "states": [
    "queued",
    "running",
    "waiting-for-approval",
    "succeeded",
    "failed",
    "cancelled"
  ],
  "files": [
    {
      "path": "agent/agent-run.ts",
      "content": "// Normalized agent run state and the event contract that updates it.\n// Framework- and provider-independent: map your backend's stream to these events,\n// or replace a run wholesale with a snapshot from your API.\n\nexport type RunStatus = \"queued\" | \"running\" | \"waiting-for-approval\" | \"succeeded\" | \"failed\" | \"cancelled\";\nexport type StepStatus = \"queued\" | \"running\" | \"succeeded\" | \"failed\" | \"skipped\";\nexport type ApprovalStatus = \"pending\" | \"approved\" | \"rejected\" | \"expired\";\nexport type MessageStatus = \"streaming\" | \"complete\" | \"failed\";\nexport type ArtifactStatus = \"generating\" | \"ready\" | \"failed\";\n\nexport interface RunStep {\n  id: string;\n  /** Tool name for tool calls, or a short label for other steps. */\n  name: string;\n  kind: \"tool\" | \"step\";\n  status: StepStatus;\n  summary?: string;\n  input?: unknown;\n  output?: string;\n  error?: string;\n  startedAt?: string;\n  finishedAt?: string;\n}\n\nexport interface RunApproval {\n  id: string;\n  title: string;\n  description?: string;\n  /** What will happen if approved, e.g. a command or diff. Rendered as text, never as HTML. */\n  detail?: string;\n  status: ApprovalStatus;\n  expiresAt?: string;\n  decidedAt?: string;\n}\n\nexport interface RunMessage {\n  id: string;\n  role: \"user\" | \"assistant\";\n  content: string;\n  status: MessageStatus;\n  error?: string;\n}\n\nexport interface RunArtifact {\n  id: string;\n  name: string;\n  /** MIME type, e.g. text/markdown, text/csv, image/png. */\n  type: string;\n  status: ArtifactStatus;\n  /** Where the artifact can be opened or downloaded. */\n  path?: string;\n  /** Inline text content for previews of text artifacts. */\n  content?: string;\n  sizeBytes?: number;\n  error?: string;\n}\n\nexport interface AgentRun {\n  id: string;\n  title: string;\n  status: RunStatus;\n  steps: RunStep[];\n  approvals: RunApproval[];\n  messages: RunMessage[];\n  artifacts: RunArtifact[];\n  createdAt?: string;\n  finishedAt?: string;\n  error?: string;\n}\n\ninterface EventBase<Type extends string, Payload> {\n  /** Unique event id; repeated deliveries are ignored. */\n  id: string;\n  runId: string;\n  /** Increases by one per event within a run. */\n  sequence: number;\n  /** ISO 8601 time the backend produced the event. */\n  timestamp: string;\n  type: Type;\n  payload: Payload;\n}\n\nexport type AgentRunEvent =\n  | EventBase<\"run.status\", { status: RunStatus; error?: string }>\n  | EventBase<\"step.started\", { step: Omit<RunStep, \"status\" | \"startedAt\"> }>\n  | EventBase<\"step.updated\", { stepId: string; status: StepStatus; output?: string; error?: string }>\n  | EventBase<\"approval.requested\", { approval: Omit<RunApproval, \"status\"> }>\n  | EventBase<\"approval.resolved\", { approvalId: string; status: Exclude<ApprovalStatus, \"pending\"> }>\n  | EventBase<\"message.started\", { message: Omit<RunMessage, \"status\" | \"content\"> & { content?: string } }>\n  | EventBase<\"message.delta\", { messageId: string; delta: string }>\n  | EventBase<\"message.completed\", { messageId: string; status: Exclude<MessageStatus, \"streaming\">; error?: string }>\n  | EventBase<\"artifact.updated\", { artifact: RunArtifact }>;\n\n/** A run plus the bookkeeping needed to apply events safely. */\nexport interface RunStore {\n  run: AgentRun;\n  /** Highest sequence applied without a gap. */\n  sequence: number;\n  /** Event ids already applied or buffered, for de-duplication. */\n  seen: string[];\n  /** Events that arrived ahead of a missing sequence, waiting for the gap to fill. */\n  pending: AgentRunEvent[];\n  /** True when events are missing; refresh from a snapshot (see `fromSnapshot`). */\n  stale: boolean;\n}\n\n/** How many out-of-order events to hold before declaring the store stale. */\nconst MAX_PENDING = 50;\n\n/** Starts a store from a run snapshot and the sequence it reflects. */\nexport function fromSnapshot(run: AgentRun, sequence = 0): RunStore {\n  return { run, sequence, seen: [], pending: [], stale: false };\n}\n\n/**\n * Applies events: ignores duplicates (by id) and other runs, buffers events that\n * arrive early, applies them in sequence order, and marks the store stale if a\n * gap does not fill. Pure: returns a new store and never mutates its input.\n */\nexport function applyEvents(store: RunStore, events: AgentRunEvent[]): RunStore {\n  const seen = new Set(store.seen);\n  let pending = [...store.pending];\n  for (const event of events) {\n    if (event.runId !== store.run.id || seen.has(event.id) || event.sequence <= store.sequence) continue;\n    seen.add(event.id);\n    pending.push(event);\n  }\n  pending.sort((a, b) => a.sequence - b.sequence);\n\n  let { run, sequence } = store;\n  while (pending.length && pending[0]!.sequence === sequence + 1) {\n    run = reduce(run, pending.shift()!);\n    sequence += 1;\n  }\n  // Drop stale duplicates of already applied sequences that slipped in under another id.\n  pending = pending.filter((event) => event.sequence > sequence);\n\n  return { run, sequence, seen: [...seen].slice(-500), pending, stale: store.stale || pending.length > MAX_PENDING };\n}\n\n/** True when events after `sequence` are buffered behind a missing one. */\nexport function hasGap(store: RunStore): boolean {\n  return store.pending.length > 0;\n}\n\nfunction reduce(run: AgentRun, event: AgentRunEvent): AgentRun {\n  const at = event.timestamp;\n  switch (event.type) {\n    case \"run.status\":\n      return {\n        ...run,\n        status: event.payload.status,\n        error: event.payload.error ?? run.error,\n        finishedAt: [\"succeeded\", \"failed\", \"cancelled\"].includes(event.payload.status) ? at : run.finishedAt,\n      };\n    case \"step.started\":\n      return upsert(run, \"steps\", { ...event.payload.step, status: \"running\", startedAt: at });\n    case \"step.updated\":\n      return patch(run, \"steps\", event.payload.stepId, (step) => ({\n        ...step,\n        status: event.payload.status,\n        output: event.payload.output ?? step.output,\n        error: event.payload.error ?? step.error,\n        finishedAt: event.payload.status === \"running\" ? step.finishedAt : at,\n      }));\n    case \"approval.requested\":\n      return upsert(run, \"approvals\", { ...event.payload.approval, status: \"pending\" });\n    case \"approval.resolved\":\n      return patch(run, \"approvals\", event.payload.approvalId, (approval) => ({ ...approval, status: event.payload.status, decidedAt: at }));\n    case \"message.started\":\n      return upsert(run, \"messages\", { content: \"\", ...event.payload.message, status: \"streaming\" });\n    case \"message.delta\":\n      return patch(run, \"messages\", event.payload.messageId, (message) => ({ ...message, content: message.content + event.payload.delta }));\n    case \"message.completed\":\n      return patch(run, \"messages\", event.payload.messageId, (message) => ({ ...message, status: event.payload.status, error: event.payload.error }));\n    case \"artifact.updated\":\n      return upsert(run, \"artifacts\", event.payload.artifact);\n  }\n}\n\ntype Collection = \"steps\" | \"approvals\" | \"messages\" | \"artifacts\";\ntype Entry<K extends Collection> = AgentRun[K][number];\n\nfunction upsert<K extends Collection>(run: AgentRun, key: K, entry: Entry<K>): AgentRun {\n  const list = run[key] as Entry<K>[];\n  const index = list.findIndex((item) => item.id === entry.id);\n  const next = index === -1 ? [...list, entry] : list.map((item, i) => (i === index ? { ...item, ...entry } : item));\n  return { ...run, [key]: next };\n}\n\nfunction patch<K extends Collection>(run: AgentRun, key: K, id: string, update: (entry: Entry<K>) => Entry<K>): AgentRun {\n  const list = run[key] as Entry<K>[];\n  return { ...run, [key]: list.map((item) => (item.id === id ? update(item) : item)) };\n}\n"
    }
  ],
  "example": {
    "content": "import { type AgentRunEvent, applyEvents, fromSnapshot } from \"@/components/seamless/agent/agent-run\";\n\nconst events: AgentRunEvent[] = [\n  { id: \"e1\", runId: \"run_1\", sequence: 1, timestamp: \"2026-11-14T10:00:00.000Z\", type: \"run.status\", payload: { status: \"running\" } },\n  { id: \"e3\", runId: \"run_1\", sequence: 3, timestamp: \"2026-11-14T10:00:02.000Z\", type: \"step.updated\", payload: { stepId: \"s1\", status: \"succeeded\", output: \"Found 3 pages.\" } },\n  { id: \"e2\", runId: \"run_1\", sequence: 2, timestamp: \"2026-11-14T10:00:01.000Z\", type: \"step.started\", payload: { step: { id: \"s1\", name: \"search_docs\", kind: \"tool\" } } },\n  { id: \"e2\", runId: \"run_1\", sequence: 2, timestamp: \"2026-11-14T10:00:01.000Z\", type: \"step.started\", payload: { step: { id: \"s1\", name: \"search_docs\", kind: \"tool\" } } },\n];\n\nexport default function AgentRunExample() {\n  // Out of order and duplicated delivery still produces one consistent run.\n  const store = applyEvents(fromSnapshot({ id: \"run_1\", title: \"Research\", status: \"queued\", steps: [], approvals: [], messages: [], artifacts: [] }), events);\n  return (\n    <pre className=\"w-full max-w-md overflow-x-auto rounded-lg border border-zinc-950/[0.07] bg-zinc-950/[0.02] px-3 py-2.5 font-mono text-xs leading-relaxed text-zinc-800 dark:border-white/[0.08] dark:bg-transparent dark:text-zinc-300\">\n      {JSON.stringify({ status: store.run.status, sequence: store.sequence, steps: store.run.steps.map((s) => `${s.name}: ${s.status}`) }, null, 2)}\n    </pre>\n  );\n}\n"
  }
}
