{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "loop-agent",
  "title": "Loop Agent",
  "description": "A support triage agent with parallel context gathering, SLA reasoning, and human approval.",
  "dependencies": [
    "@ai-sdk/react",
    "@base-ui/react",
    "@phosphor-icons/react",
    "ai",
    "class-variance-authority",
    "lucide-react",
    "zod"
  ],
  "registryDependencies": [
    "utils",
    "badge",
    "breadcrumb",
    "button",
    "textarea",
    "https://elements.ai-sdk.dev/api/registry/chain-of-thought.json",
    "https://elements.ai-sdk.dev/api/registry/confirmation.json",
    "https://elements.ai-sdk.dev/api/registry/conversation.json",
    "https://elements.ai-sdk.dev/api/registry/message.json",
    "https://elements.ai-sdk.dev/api/registry/reasoning.json",
    "https://elements.ai-sdk.dev/api/registry/shimmer.json",
    "https://elements.ai-sdk.dev/api/registry/tool.json"
  ],
  "files": [
    {
      "path": "registry/loop-agent/app/demos/loop-agent/page.tsx",
      "content": "import { LoopAgentScreen } from \"@/components/loop-agent/loop-agent-screen\";\n\nexport default function LoopAgentPage() {\n  return <LoopAgentScreen />;\n}\n",
      "type": "registry:page",
      "target": "app/demos/loop-agent/page.tsx"
    },
    {
      "path": "registry/loop-agent/app/api/demos/loop-agent/route.ts",
      "content": "import { handleLoopAgentRequest } from \"@/lib/loop-agent/runtime\";\n\nexport const runtime = \"nodejs\";\n\nexport function POST(request: Request) {\n  return handleLoopAgentRequest(request);\n}\n",
      "type": "registry:page",
      "target": "app/api/demos/loop-agent/route.ts"
    },
    {
      "path": "registry/loop-agent/components/loop-agent/loop-agent-screen.tsx",
      "content": "import { Badge } from \"@/components/ui/badge\";\nimport {\n  Breadcrumb,\n  BreadcrumbItem,\n  BreadcrumbLink,\n  BreadcrumbList,\n  BreadcrumbPage,\n  BreadcrumbSeparator,\n} from \"@/components/ui/breadcrumb\";\nimport { ArrowLeft } from \"lucide-react\";\n\nimport { getLoopAgentRuntimeState } from \"@/lib/loop-agent/runtime\";\nimport { runSupportTriageLoop } from \"@/lib/loop-agent/support-triage\";\nimport { LoopAgentWorkspace } from \"@/components/loop-agent/loop-agent-workspace\";\n\nexport function LoopAgentScreen() {\n  const runtimeState = getLoopAgentRuntimeState();\n  const triage = runSupportTriageLoop();\n\n  return (\n    <main className=\"min-h-svh bg-background text-foreground\">\n      <div className=\"mx-auto flex w-full max-w-7xl flex-col gap-6 px-4 py-6 md:px-6\">\n        <header className=\"grid gap-4 border border-foreground/10 bg-background px-4 py-5 md:grid-cols-[minmax(0,1fr)_auto] md:items-end\">\n          <div className=\"space-y-2\">\n            <Breadcrumb>\n              <BreadcrumbList className=\"text-[11px] text-muted-foreground uppercase tracking-[0.2em]\">\n                <BreadcrumbItem>\n                  <BreadcrumbLink\n                    aria-label=\"Back to demos\"\n                    className=\"-ml-1 inline-flex items-center gap-1.5 text-muted-foreground transition-colors hover:text-foreground\"\n                    href=\"/\"\n                  >\n                    <ArrowLeft aria-hidden=\"true\" className=\"size-3.5 shrink-0\" />\n                    <span>Demo</span>\n                  </BreadcrumbLink>\n                </BreadcrumbItem>\n                <BreadcrumbSeparator className=\"text-muted-foreground\">\n                  /\n                </BreadcrumbSeparator>\n                <BreadcrumbItem>\n                  <BreadcrumbPage className=\"font-normal text-muted-foreground\">\n                    Loop Agent\n                  </BreadcrumbPage>\n                </BreadcrumbItem>\n              </BreadcrumbList>\n            </Breadcrumb>\n            <h1 className=\"max-w-3xl font-medium text-2xl tracking-tight\">\n              Multi-step support triage agent with human approval\n            </h1>\n            <p className=\"max-w-3xl text-muted-foreground text-sm/relaxed\">\n              This slice turns the official AI SDK tool-calling and loop-control\n              recipes into an inspectable support workflow: independent context\n              lookups, a dependent SLA decision, a human approval checkpoint,\n              and a bounded agent loop.\n            </p>\n          </div>\n\n          <div className=\"flex flex-wrap items-center gap-2\">\n            <Badge variant=\"outline\">{runtimeState.statusLabel}</Badge>\n            <Badge variant=\"outline\">{runtimeState.chatModel}</Badge>\n          </div>\n        </header>\n\n        <div className=\"lg:h-svh\">\n          <LoopAgentWorkspace\n            chatModel={runtimeState.chatModel}\n            isChatAvailable={runtimeState.isChatAvailable}\n            nodeVersion={runtimeState.nodeVersion}\n            setupMessage={runtimeState.setupMessage}\n            triage={triage}\n          />\n        </div>\n      </div>\n    </main>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/loop-agent/loop-agent-screen.tsx"
    },
    {
      "path": "registry/loop-agent/components/loop-agent/loop-agent-workspace.tsx",
      "content": "\"use client\";\n\nimport { Chat } from \"@ai-sdk/react\";\nimport {\n  ArrowClockwiseIcon,\n  CheckCircleIcon,\n  GitBranchIcon,\n  StopIcon,\n  WrenchIcon,\n  XCircleIcon,\n} from \"@phosphor-icons/react\";\nimport {\n  ChainOfThought,\n  ChainOfThoughtContent,\n  ChainOfThoughtHeader,\n  ChainOfThoughtSearchResult,\n  ChainOfThoughtSearchResults,\n  ChainOfThoughtStep,\n} from \"@/components/ai-elements/chain-of-thought\";\nimport {\n  Confirmation,\n  ConfirmationAccepted,\n  ConfirmationAction,\n  ConfirmationActions,\n  ConfirmationRejected,\n  ConfirmationRequest,\n  ConfirmationTitle,\n} from \"@/components/ai-elements/confirmation\";\nimport {\n  Conversation,\n  ConversationContent,\n  ConversationEmptyState,\n  ConversationScrollButton,\n} from \"@/components/ai-elements/conversation\";\nimport {\n  Message,\n  MessageContent,\n  MessageResponse,\n} from \"@/components/ai-elements/message\";\nimport {\n  PromptInput,\n  PromptInputBody,\n  PromptInputFooter,\n  PromptInputSubmit,\n  PromptInputTextarea,\n} from \"@/components/ai-elements/prompt-input\";\nimport {\n  Reasoning,\n  ReasoningContent,\n  ReasoningTrigger,\n} from \"@/components/ai-elements/reasoning\";\nimport { Shimmer } from \"@/components/ai-elements/shimmer\";\nimport {\n  Tool,\n  ToolContent,\n  ToolHeader,\n  ToolInput,\n  ToolOutput,\n  type ToolPart,\n} from \"@/components/ai-elements/tool\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  type ChatAddToolApproveResponseFunction,\n  isReasoningUIPart,\n  isToolUIPart,\n  type UIMessage,\n} from \"ai\";\nimport { type ReactNode, useMemo } from \"react\";\n\nimport type { SupportTriageResult } from \"@/lib/loop-agent/support-triage\";\nimport { useLoopAgentChat } from \"./use-loop-agent-chat\";\n\nfunction getTextContent(message: UIMessage) {\n  return message.parts\n    .filter((part) => part.type === \"text\")\n    .map((part) => part.text)\n    .join(\"\\n\");\n}\n\nfunction getToolParts(message: UIMessage) {\n  return message.parts.filter(isToolUIPart);\n}\n\nfunction getReasoningText(message: UIMessage) {\n  return message.parts\n    .filter(isReasoningUIPart)\n    .map((part) => part.text)\n    .join(\"\\n\\n\");\n}\n\nfunction getToolName(part: ToolPart) {\n  return part.type === \"dynamic-tool\"\n    ? part.toolName\n    : part.type.split(\"-\").slice(1).join(\"-\");\n}\n\nfunction getBatchStatus(toolNames: string[], toolParts: ToolPart[]) {\n  const batchParts = toolParts.filter((part) =>\n    toolNames.includes(getToolName(part))\n  );\n\n  if (batchParts.length === 0) {\n    return \"pending\" as const;\n  }\n\n  const isComplete = batchParts.every(\n    (part) =>\n      part.state === \"output-available\" || part.state === \"output-denied\"\n  );\n\n  if (isComplete) {\n    return \"complete\" as const;\n  }\n\n  return \"active\" as const;\n}\n\nfunction getBatchDescription(\n  batch: SupportTriageResult[\"toolBatches\"][number],\n  triage: SupportTriageResult\n) {\n  if (batch.tools.includes(\"requestHumanApproval\")) {\n    return `Requests human approval before escalating ${triage.caseId}.`;\n  }\n\n  if (batch.execution === \"parallel\") {\n    return `Checks account context in parallel before the SLA decision for ${triage.caseId}.`;\n  }\n\n  return `Uses the completed account context to assess SLA risk for ${triage.caseId}.`;\n}\n\ninterface ApprovalToolInput {\n  action?: string;\n  caseId?: string;\n  customerName?: string;\n  customerUpdate?: string;\n  internalHandoff?: string;\n  priority?: string;\n  rationale?: string[];\n}\n\nfunction getApprovalToolInput(part: ToolPart): ApprovalToolInput {\n  if (\n    typeof part.input !== \"object\" ||\n    part.input === null ||\n    Array.isArray(part.input)\n  ) {\n    return {};\n  }\n\n  return part.input as ApprovalToolInput;\n}\n\ninterface HumanApprovalConfirmationProps {\n  onApprovalResponse: ChatAddToolApproveResponseFunction;\n  part: ToolPart;\n  triage: SupportTriageResult;\n}\n\nfunction HumanApprovalConfirmation({\n  onApprovalResponse,\n  part,\n  triage,\n}: HumanApprovalConfirmationProps) {\n  const approval = part.approval;\n\n  if (!approval) {\n    return null;\n  }\n\n  const input = getApprovalToolInput(part);\n  const target = input.customerName\n    ? `${input.customerName} / ${input.caseId ?? triage.caseId}`\n    : (input.caseId ?? triage.caseId);\n  const action = input.action ?? triage.recommendation.action;\n  const priority = input.priority ?? triage.recommendation.priority;\n\n  return (\n    <Confirmation\n      approval={approval}\n      className=\"border-amber-500/30 bg-amber-500/5\"\n      state={part.state}\n    >\n      <ConfirmationTitle>Human approval checkpoint</ConfirmationTitle>\n      <ConfirmationRequest>\n        <div className=\"space-y-2 text-sm\">\n          <p>\n            Approve {priority} priority {action} for {target}?\n          </p>\n          {input.customerUpdate ? (\n            <p className=\"text-muted-foreground\">{input.customerUpdate}</p>\n          ) : null}\n          {input.rationale?.length ? (\n            <ul className=\"list-disc space-y-1 pl-5 text-muted-foreground\">\n              {input.rationale.map((reason) => (\n                <li key={reason}>{reason}</li>\n              ))}\n            </ul>\n          ) : null}\n        </div>\n      </ConfirmationRequest>\n      <ConfirmationAccepted>\n        <CheckCircleIcon className=\"size-4\" />\n        <span>Approved. The agent can execute the escalation handoff.</span>\n      </ConfirmationAccepted>\n      <ConfirmationRejected>\n        <XCircleIcon className=\"size-4\" />\n        <span>Rejected. The agent will continue without escalating.</span>\n      </ConfirmationRejected>\n      <ConfirmationActions>\n        <ConfirmationAction\n          onClick={() =>\n            onApprovalResponse({\n              approved: false,\n              id: approval.id,\n              reason: \"Reviewer rejected the support escalation.\",\n            })\n          }\n          variant=\"outline\"\n        >\n          <XCircleIcon className=\"size-3.5\" />\n          Reject\n        </ConfirmationAction>\n        <ConfirmationAction\n          onClick={() =>\n            onApprovalResponse({\n              approved: true,\n              id: approval.id,\n              reason: \"Reviewer approved the support escalation.\",\n            })\n          }\n        >\n          <CheckCircleIcon className=\"size-3.5\" />\n          Approve\n        </ConfirmationAction>\n      </ConfirmationActions>\n    </Confirmation>\n  );\n}\n\nexport interface LoopAgentWorkspaceProps {\n  chatModel: string;\n  isChatAvailable: boolean;\n  nodeVersion: string;\n  setupMessage: string | null;\n  triage: SupportTriageResult;\n}\n\ninterface AssistantTraceProps {\n  isLastMessage: boolean;\n  isStreaming: boolean;\n  message: UIMessage;\n  onApprovalResponse: ChatAddToolApproveResponseFunction;\n  triage: SupportTriageResult;\n}\n\nfunction AssistantTrace({\n  isLastMessage,\n  isStreaming,\n  message,\n  onApprovalResponse,\n  triage,\n}: AssistantTraceProps) {\n  const text = getTextContent(message);\n  const reasoningText = getReasoningText(message);\n  const toolParts = getToolParts(message);\n  const hasReasoning = reasoningText.length > 0;\n  const hasText = text.length > 0;\n  const lastPart = message.parts.at(-1);\n  const isReasoningStreaming =\n    isLastMessage && isStreaming && lastPart?.type === \"reasoning\";\n  const showBodyThinking =\n    isLastMessage && isStreaming && !hasReasoning && !hasText;\n  let bodyContent: ReactNode = null;\n\n  if (hasText) {\n    bodyContent = <MessageResponse>{text}</MessageResponse>;\n  } else if (showBodyThinking) {\n    bodyContent = <Shimmer className=\"text-sm\">Thinking...</Shimmer>;\n  }\n\n  return (\n    <>\n      {hasReasoning ? (\n        <Reasoning className=\"w-full\" isStreaming={isReasoningStreaming}>\n          <ReasoningTrigger />\n          <ReasoningContent>{reasoningText}</ReasoningContent>\n        </Reasoning>\n      ) : null}\n\n      {toolParts.length > 0 ? (\n        <ChainOfThought\n          className=\"rounded-md border border-foreground/10 bg-muted/20 px-3 py-3\"\n          defaultOpen\n        >\n          <ChainOfThoughtHeader>Verification steps</ChainOfThoughtHeader>\n          <ChainOfThoughtContent>\n            {triage.toolBatches.map((batch) => (\n              <ChainOfThoughtStep\n                description={getBatchDescription(batch, triage)}\n                key={batch.label}\n                label={batch.label}\n                status={getBatchStatus(batch.tools, toolParts)}\n              >\n                <ChainOfThoughtSearchResults>\n                  {batch.tools.map((toolName) => (\n                    <ChainOfThoughtSearchResult key={toolName}>\n                      {toolName}\n                    </ChainOfThoughtSearchResult>\n                  ))}\n                </ChainOfThoughtSearchResults>\n              </ChainOfThoughtStep>\n            ))}\n\n            <ChainOfThoughtStep\n              description={`${triage.recommendation.priority} priority ${triage.recommendation.action} for ${triage.caseId}.`}\n              label=\"Prepare recommendation\"\n              status={text ? \"complete\" : \"active\"}\n            />\n          </ChainOfThoughtContent>\n        </ChainOfThought>\n      ) : null}\n\n      {toolParts.map((part) => (\n        <div className=\"space-y-3\" key={part.toolCallId}>\n          <HumanApprovalConfirmation\n            onApprovalResponse={onApprovalResponse}\n            part={part}\n            triage={triage}\n          />\n          <Tool>\n            {part.type === \"dynamic-tool\" ? (\n              <ToolHeader\n                state={part.state}\n                title=\"Support Triage Tool\"\n                toolName={part.toolName}\n                type={part.type}\n              />\n            ) : (\n              <ToolHeader\n                state={part.state}\n                title=\"Support Triage Tool\"\n                type={part.type}\n              />\n            )}\n            <ToolContent>\n              {part.input ? <ToolInput input={part.input} /> : null}\n              <ToolOutput errorText={part.errorText} output={part.output} />\n            </ToolContent>\n          </Tool>\n        </div>\n      ))}\n\n      {bodyContent}\n    </>\n  );\n}\n\nexport function LoopAgentWorkspace({\n  chatModel,\n  isChatAvailable,\n  nodeVersion,\n  setupMessage,\n  triage,\n}: LoopAgentWorkspaceProps) {\n  const {\n    addToolApprovalResponse,\n    error,\n    hasMessages,\n    isBusy,\n    messages,\n    regenerate,\n    sendMessage,\n    status,\n    stop,\n  } = useLoopAgentChat();\n  const samplePrompts = useMemo(\n    () => [\n      `Triage ${triage.caseId} and explain the tool sequence.`,\n      `Triage ${triage.caseId} and show which lookups ran in parallel before the SLA decision.`,\n      `Triage ${triage.caseId}, request human approval before escalation, then finalize.`,\n    ],\n    [triage.caseId]\n  );\n\n  return (\n    <div className=\"grid min-h-[70svh] gap-4 lg:h-full lg:min-h-0 lg:grid-cols-[minmax(0,1fr)_22rem]\">\n      <section className=\"flex min-h-[70svh] flex-col border border-foreground/10 bg-background lg:h-full lg:min-h-0\">\n        {isChatAvailable ? null : (\n          <div className=\"border-foreground/10 border-b px-4 py-3 text-muted-foreground text-xs/relaxed\">\n            {setupMessage}\n          </div>\n        )}\n\n        {error ? (\n          <div className=\"border-foreground/10 border-b px-4 py-3 text-destructive text-xs/relaxed\">\n            {error.message}\n          </div>\n        ) : null}\n\n        <Conversation className=\"min-h-0\">\n          <ConversationContent className=\"mx-auto flex w-full max-w-3xl flex-1 gap-6 px-4 py-6\">\n            {hasMessages ? (\n              messages.map((message, index) => (\n                <Message from={message.role} key={message.id}>\n                  <MessageContent\n                    className={cn(\n                      \"space-y-4\",\n                      message.role === \"assistant\" ? \"max-w-3xl\" : \"max-w-2xl\"\n                    )}\n                  >\n                    {message.role === \"assistant\" ? (\n                      <AssistantTrace\n                        isLastMessage={index === messages.length - 1}\n                        isStreaming={isBusy}\n                        message={message}\n                        onApprovalResponse={addToolApprovalResponse}\n                        triage={triage}\n                      />\n                    ) : (\n                      <MessageResponse>\n                        {getTextContent(message)}\n                      </MessageResponse>\n                    )}\n                  </MessageContent>\n                </Message>\n              ))\n            ) : (\n              <ConversationEmptyState\n                description=\"Ask the agent to triage the default support case and inspect which tools run before the final recommendation.\"\n                icon={<GitBranchIcon className=\"size-5\" />}\n                title=\"Support triage loop is ready\"\n              />\n            )}\n          </ConversationContent>\n          <ConversationScrollButton />\n        </Conversation>\n\n        <div className=\"border-foreground/10 border-t px-4 py-4\">\n          <div className=\"mx-auto w-full max-w-3xl\">\n            <PromptInput onSubmit={({ text }) => sendMessage({ text })}>\n              <PromptInputBody>\n                <PromptInputTextarea\n                  disabled={!isChatAvailable || isBusy}\n                  placeholder=\"Ask the agent to triage the default support case.\"\n                />\n              </PromptInputBody>\n              <PromptInputFooter className=\"flex items-center justify-between gap-3 border-foreground/10 border-t px-3 py-3\">\n                <div className=\"flex flex-wrap items-center gap-2\">\n                  <Badge variant=\"outline\">ToolLoopAgent</Badge>\n                  <Badge variant=\"outline\">Tools</Badge>\n                  <Badge variant=\"outline\">Human approval</Badge>\n                  <Badge variant=\"outline\">{chatModel}</Badge>\n                </div>\n                <div className=\"flex items-center gap-2\">\n                  {isBusy ? (\n                    <Button\n                      onClick={stop}\n                      size=\"sm\"\n                      type=\"button\"\n                      variant=\"outline\"\n                    >\n                      <StopIcon className=\"size-3.5\" />\n                      Stop\n                    </Button>\n                  ) : null}\n                  {hasMessages ? (\n                    <Button\n                      onClick={() => regenerate()}\n                      size=\"sm\"\n                      type=\"button\"\n                      variant=\"outline\"\n                    >\n                      <ArrowClockwiseIcon className=\"size-3.5\" />\n                      Retry\n                    </Button>\n                  ) : null}\n                  <PromptInputSubmit\n                    disabled={!isChatAvailable}\n                    status={status}\n                  />\n                </div>\n              </PromptInputFooter>\n            </PromptInput>\n\n            {hasMessages ? null : (\n              <div className=\"mt-3 flex flex-wrap gap-2\">\n                {samplePrompts.map((prompt) => (\n                  <Button\n                    key={prompt}\n                    onClick={() => sendMessage({ text: prompt })}\n                    size=\"sm\"\n                    type=\"button\"\n                    variant=\"outline\"\n                  >\n                    <WrenchIcon className=\"size-3.5\" />\n                    {prompt}\n                  </Button>\n                ))}\n              </div>\n            )}\n          </div>\n        </div>\n      </section>\n\n      <aside className=\"border border-foreground/10 bg-background p-4 lg:min-h-0 lg:overflow-y-auto\">\n        <div className=\"space-y-5\">\n          <div>\n            <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.2em]\">\n              Runtime\n            </p>\n            <p className=\"mt-1 font-medium text-sm\">{nodeVersion}</p>\n          </div>\n\n          <div>\n            <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.2em]\">\n              Active case\n            </p>\n            <p className=\"mt-1 font-medium text-sm\">{triage.caseId}</p>\n            <p className=\"mt-1 text-muted-foreground text-sm\">\n              {triage.ticket.title}\n            </p>\n          </div>\n\n          <div className=\"grid gap-2\">\n            <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.2em]\">\n              Tool plan\n            </p>\n            {triage.toolBatches.map((batch) => (\n              <div\n                className=\"border border-foreground/10 p-3\"\n                key={batch.label}\n              >\n                <div className=\"flex items-center justify-between gap-3\">\n                  <p className=\"font-medium text-sm\">{batch.label}</p>\n                  <Badge variant=\"outline\">{batch.execution}</Badge>\n                </div>\n                <div className=\"mt-2 flex flex-wrap gap-1.5\">\n                  {batch.tools.map((toolName) => (\n                    <Badge key={toolName} variant=\"secondary\">\n                      {toolName}\n                    </Badge>\n                  ))}\n                </div>\n              </div>\n            ))}\n          </div>\n\n          <div>\n            <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.2em]\">\n              Recommendation\n            </p>\n            <p className=\"mt-1 font-medium text-sm\">\n              {triage.recommendation.action} / {triage.recommendation.priority}\n            </p>\n            <p className=\"mt-1 text-muted-foreground text-sm\">\n              {triage.risk.reason}\n            </p>\n          </div>\n        </div>\n      </aside>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/loop-agent/loop-agent-workspace.tsx"
    },
    {
      "path": "registry/loop-agent/components/loop-agent/use-loop-agent-chat.ts",
      "content": "\"use client\";\n\nimport { Chat, useChat } from \"@ai-sdk/react\";\nimport {\n  DefaultChatTransport,\n  lastAssistantMessageIsCompleteWithApprovalResponses,\n} from \"ai\";\nimport { useState } from \"react\";\n\nexport function useLoopAgentChat() {\n  const [chat] = useState(\n    () =>\n      new Chat({\n        sendAutomaticallyWhen:\n          lastAssistantMessageIsCompleteWithApprovalResponses,\n        transport: new DefaultChatTransport({\n          api: \"/api/demos/loop-agent\",\n        }),\n      })\n  );\n  const controller = useChat({ chat });\n  const hasMessages = controller.messages.length > 0;\n  const isBusy =\n    controller.status === \"submitted\" || controller.status === \"streaming\";\n\n  return {\n    ...controller,\n    chat,\n    hasMessages,\n    isBusy,\n  };\n}\n",
      "type": "registry:component",
      "target": "@components/loop-agent/use-loop-agent-chat.ts"
    },
    {
      "path": "registry/loop-agent/components/ai-elements/prompt-input.tsx",
      "content": "\"use client\";\n\nimport { CornerDownLeftIcon, LoaderCircleIcon, SquareIcon } from \"lucide-react\";\nimport type {\n  ComponentProps,\n  FormEvent,\n  HTMLAttributes,\n  ReactNode,\n  TextareaHTMLAttributes,\n} from \"react\";\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useMemo,\n  useState,\n} from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { cn } from \"@/lib/utils\";\n\ntype PromptStatus = \"error\" | \"ready\" | \"streaming\" | \"submitted\";\n\ninterface PromptInputContextValue {\n  setText: (text: string) => void;\n  text: string;\n}\n\ninterface PromptInputMessage {\n  text: string;\n}\n\ninterface PromptInputProps\n  extends Omit<HTMLAttributes<HTMLFormElement>, \"onSubmit\"> {\n  children: ReactNode;\n  onSubmit: (\n    message: PromptInputMessage,\n    event: FormEvent<HTMLFormElement>\n  ) => void | Promise<void>;\n}\n\ninterface PromptInputSubmitProps\n  extends Omit<ComponentProps<typeof Button>, \"children\" | \"type\"> {\n  onStop?: () => void;\n  status?: PromptStatus;\n}\n\ntype PromptInputClickEvent = Parameters<\n  Exclude<ComponentProps<typeof Button>[\"onClick\"], undefined>\n>[0];\n\nconst PromptInputContext = createContext<PromptInputContextValue | null>(null);\n\nfunction usePromptInputContext() {\n  const context = useContext(PromptInputContext);\n\n  if (!context) {\n    throw new Error(\n      \"PromptInput components must be used inside <PromptInput>.\"\n    );\n  }\n\n  return context;\n}\n\nexport function PromptInput({\n  children,\n  className,\n  onSubmit,\n  ...props\n}: PromptInputProps) {\n  const [text, setText] = useState(\"\");\n\n  const contextValue = useMemo(\n    () => ({\n      setText,\n      text,\n    }),\n    [text]\n  );\n\n  const handleSubmit = useCallback(\n    async (event: FormEvent<HTMLFormElement>) => {\n      event.preventDefault();\n\n      const nextText = text.trim();\n      if (!nextText) {\n        return;\n      }\n\n      const result = onSubmit({ text: nextText }, event);\n\n      try {\n        await result;\n        setText(\"\");\n      } catch {\n        // Keep the text for retry after a failed submit.\n      }\n    },\n    [onSubmit, text]\n  );\n\n  return (\n    <PromptInputContext.Provider value={contextValue}>\n      <form\n        className={cn(\"w-full\", className)}\n        onSubmit={handleSubmit}\n        {...props}\n      >\n        {children}\n      </form>\n    </PromptInputContext.Provider>\n  );\n}\n\nexport function PromptInputBody({\n  className,\n  ...props\n}: HTMLAttributes<HTMLDivElement>) {\n  return <div className={cn(\"grid gap-3\", className)} {...props} />;\n}\n\nexport function PromptInputFooter({\n  className,\n  ...props\n}: HTMLAttributes<HTMLDivElement>) {\n  return <div className={cn(className)} {...props} />;\n}\n\nexport function PromptInputTextarea({\n  className,\n  disabled,\n  onChange,\n  ...props\n}: TextareaHTMLAttributes<HTMLTextAreaElement>) {\n  const context = usePromptInputContext();\n\n  return (\n    <Textarea\n      className={cn(\"min-h-16 resize-none\", className)}\n      disabled={disabled}\n      onChange={(event) => {\n        context.setText(event.currentTarget.value);\n        onChange?.(event);\n      }}\n      value={context.text}\n      {...props}\n    />\n  );\n}\n\nexport function PromptInputSubmit({\n  className,\n  disabled,\n  onClick,\n  onStop,\n  size = \"icon\",\n  status = \"ready\",\n  variant = \"default\",\n  ...props\n}: PromptInputSubmitProps) {\n  const { text } = usePromptInputContext();\n  const isBusy = status === \"submitted\" || status === \"streaming\";\n  const isDisabled = Boolean(disabled) || (!isBusy && text.trim().length === 0);\n\n  let icon = <CornerDownLeftIcon className=\"size-4\" />;\n\n  if (status === \"submitted\") {\n    icon = <LoaderCircleIcon className=\"size-4 animate-spin\" />;\n  } else if (status === \"streaming\") {\n    icon = <SquareIcon className=\"size-4\" />;\n  }\n\n  return (\n    <Button\n      aria-label={isBusy ? \"Stop\" : \"Submit\"}\n      className={cn(className)}\n      disabled={isDisabled}\n      onClick={(event: PromptInputClickEvent) => {\n        if (isBusy && onStop) {\n          event.preventDefault();\n          onStop();\n          return;\n        }\n\n        onClick?.(event);\n      }}\n      size={size}\n      type={isBusy && onStop ? \"button\" : \"submit\"}\n      variant={variant}\n      {...props}\n    >\n      {icon}\n    </Button>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/ai-elements/prompt-input.tsx"
    },
    {
      "path": "registry/loop-agent/lib/loop-agent/chat.ts",
      "content": "import {\n  createAgentUIStreamResponse,\n  stepCountIs,\n  ToolLoopAgent,\n  tool,\n  type UIMessage,\n} from \"ai\";\nimport { z } from \"zod\";\n\nimport {\n  resolveLoopAgentChatModel,\n  resolveLoopAgentProviderOptions,\n} from \"./model\";\nimport {\n  createLoopAgentGateway,\n  getLoopAgentEnv,\n  type LoopAgentEnv,\n} from \"./env\";\nimport {\n  buildSupportEscalationApprovalRequest,\n  recordSupportEscalationApproval,\n  runSupportTriageLoop,\n} from \"./support-triage\";\n\nconst loopAgentInstructions = [\n  \"You are the loop-agent demo for a support operations team.\",\n  \"Triage support tickets by using tools before making a recommendation.\",\n  \"For questions about the default case, tool sequence, parallel lookups, SLA decision, approval flow, or next support action, use the tools before answering.\",\n  \"Do not answer those workflow questions from the instructions alone.\",\n  \"Start by gathering independent account context, recent tickets, and entitlement information.\",\n  \"Then calculate SLA risk before giving the final action, priority, and rationale.\",\n  \"If the SLA recommendation is a high-priority escalation, call requestHumanApproval with the approvalRequest returned by calculateSlaRisk before the final answer.\",\n  \"If approval is denied, do not retry the approval tool; explain that the escalation was not performed.\",\n  \"Keep the final answer concise and make the tool sequence easy for an engineer to inspect.\",\n].join(\" \");\n\nfunction createSupportTriageTools() {\n  return {\n    calculateSlaRisk: tool({\n      description:\n        \"Calculate the SLA risk and recommendation for the active support case after account context is known.\",\n      inputSchema: z.object({\n        caseId: z.string().describe(\"The support case identifier to triage.\"),\n      }),\n      execute: () => {\n        const triage = runSupportTriageLoop();\n\n        return {\n          approvalRequest: buildSupportEscalationApprovalRequest(triage),\n          recommendation: triage.recommendation,\n          risk: triage.risk,\n        };\n      },\n    }),\n    checkEntitlement: tool({\n      description:\n        \"Check whether the customer is entitled to priority support and escalation.\",\n      inputSchema: z.object({\n        customerId: z\n          .string()\n          .describe(\"The customer identifier from the support ticket.\"),\n      }),\n      execute: () => {\n        const triage = runSupportTriageLoop();\n\n        return {\n          plan: triage.customer.plan,\n          prioritySupport: triage.customer.plan === \"Enterprise\",\n          supportChannels: [\"chat\", \"email\", \"priority escalation\"],\n        };\n      },\n    }),\n    getCustomerProfile: tool({\n      description:\n        \"Fetch the customer profile, plan, and account context for a support ticket.\",\n      inputSchema: z.object({\n        customerId: z\n          .string()\n          .describe(\"The customer identifier from the support ticket.\"),\n      }),\n      execute: () => {\n        const triage = runSupportTriageLoop();\n\n        return triage.customer;\n      },\n    }),\n    listRecentTickets: tool({\n      description:\n        \"List recent support tickets so the agent can detect repeated incidents.\",\n      inputSchema: z.object({\n        customerId: z\n          .string()\n          .describe(\"The customer identifier from the support ticket.\"),\n      }),\n      execute: () => {\n        const triage = runSupportTriageLoop();\n\n        return {\n          activeTicket: triage.ticket,\n          recentTickets: [\n            {\n              id: \"TIC-7712\",\n              status: \"resolved\",\n              title: \"Scheduled export job exceeded retry budget\",\n            },\n          ],\n        };\n      },\n    }),\n    requestHumanApproval: tool({\n      description:\n        \"Request human approval before performing the recommended high-priority support escalation.\",\n      inputSchema: z.object({\n        action: z.literal(\"escalate\").describe(\"The approved action to take.\"),\n        caseId: z.string().describe(\"The support case identifier.\"),\n        customerName: z.string().describe(\"The customer name.\"),\n        customerUpdate: z\n          .string()\n          .describe(\"The message the support team will send to the customer.\"),\n        internalHandoff: z\n          .string()\n          .describe(\"The internal escalation handoff note.\"),\n        priority: z.literal(\"high\").describe(\"The escalation priority.\"),\n        rationale: z\n          .array(z.string())\n          .min(1)\n          .describe(\"The reasons the escalation needs human approval.\"),\n      }),\n      needsApproval: true,\n      execute: (approvalRequest) =>\n        recordSupportEscalationApproval(approvalRequest),\n    }),\n  };\n}\n\nexport function streamLoopAgent(\n  messages: UIMessage[],\n  env: LoopAgentEnv = getLoopAgentEnv()\n) {\n  const gateway = createLoopAgentGateway(env);\n  const chatModel = resolveLoopAgentChatModel(env);\n  const providerOptions = resolveLoopAgentProviderOptions(chatModel);\n  const agent = new ToolLoopAgent({\n    instructions: loopAgentInstructions,\n    model: gateway(chatModel),\n    ...(providerOptions ? { providerOptions } : {}),\n    stopWhen: stepCountIs(6),\n    tools: createSupportTriageTools(),\n  });\n\n  return createAgentUIStreamResponse({\n    agent,\n    sendReasoning: true,\n    uiMessages: messages,\n  });\n}\n",
      "type": "registry:lib",
      "target": "@lib/loop-agent/chat.ts"
    },
    {
      "path": "registry/loop-agent/lib/ai-gateway/contract.ts",
      "content": "import { createGateway } from \"ai\";\n\nexport const DEFAULT_GATEWAY_BASE_URL = \"https://ai-gateway.vercel.sh/v3/ai\";\nexport const MINIMUM_NODE_VERSION = \"22.13.0\";\nconst nodeVersionPattern = /^v?(\\d+)\\.(\\d+)\\.(\\d+)(?:[-+].*)?$/;\n\nexport type AiGatewayEnvRecord = Record<string, string | undefined>;\n\nexport interface ParsedNodeVersion {\n  major: number;\n  minor: number;\n  patch: number;\n}\n\nexport interface AiGatewayContractConfig {\n  apiKey: string;\n  baseURL: string;\n  chatModel: string;\n}\n\nexport interface AiGatewayResolvedEnv {\n  apiKey: string | undefined;\n  baseURL: string;\n  chatModel: string;\n}\n\nexport interface AiGatewaySetupConfig {\n  baseURL: string;\n  chatModel: string;\n}\n\nexport interface AiGatewayContractSetupState<\n  TConfig extends AiGatewaySetupConfig = AiGatewaySetupConfig,\n> {\n  config: TConfig;\n  isReady: boolean;\n  issues: string[];\n  nodeVersion: string;\n}\n\nexport interface AiGatewayContractOptions<\n  TConfig extends AiGatewaySetupConfig = AiGatewaySetupConfig,\n> {\n  buildConfig?: (\n    resolvedEnv: AiGatewayResolvedEnv,\n    env: AiGatewayEnvRecord\n  ) => TConfig;\n  defaultBaseURL?: string;\n  defaultChatModel: string;\n  getAdditionalIssues?: (\n    resolvedEnv: AiGatewayResolvedEnv,\n    env: AiGatewayEnvRecord\n  ) => string[];\n  missingApiKeyError: string;\n  missingApiKeyIssue?: string;\n}\n\nconst genericMissingApiKeyIssue =\n  \"AI_GATEWAY_API_KEY is missing. The demo can render, but chat requests will fail until it is configured.\";\n\nexport function parseNodeVersion(version: string): ParsedNodeVersion {\n  const match = nodeVersionPattern.exec(version);\n  const major = Number(match?.[1]);\n  const minor = Number(match?.[2]);\n  const patch = Number(match?.[3]);\n\n  if (![major, minor, patch].every(Number.isInteger)) {\n    throw new Error(`Unable to parse Node.js version: \"${version}\".`);\n  }\n\n  return { major, minor, patch };\n}\n\nexport function getNodeMajor(version: string): number {\n  return parseNodeVersion(version).major;\n}\n\nfunction compareNodeVersions(\n  left: ParsedNodeVersion,\n  right: ParsedNodeVersion\n): number {\n  if (left.major !== right.major) {\n    return left.major - right.major;\n  }\n\n  if (left.minor !== right.minor) {\n    return left.minor - right.minor;\n  }\n\n  return left.patch - right.patch;\n}\n\nexport function assertSupportedNodeRuntime(version = process.version): number {\n  const parsedVersion = parseNodeVersion(version);\n  const minimumVersion = parseNodeVersion(MINIMUM_NODE_VERSION);\n\n  if (compareNodeVersions(parsedVersion, minimumVersion) < 0) {\n    throw new Error(\n      `Node.js ${version} is unsupported. This demo workspace requires Node.js >=${MINIMUM_NODE_VERSION}.`\n    );\n  }\n\n  return parsedVersion.major;\n}\n\nexport function resolveAiGatewayContractEnv(\n  env: AiGatewayEnvRecord,\n  options: Pick<AiGatewayContractOptions, \"defaultBaseURL\" | \"defaultChatModel\">\n): AiGatewayResolvedEnv {\n  return {\n    apiKey: env.AI_GATEWAY_API_KEY,\n    baseURL:\n      env.AI_GATEWAY_BASE_URL ||\n      options.defaultBaseURL ||\n      DEFAULT_GATEWAY_BASE_URL,\n    chatModel: env.AI_GATEWAY_CHAT_MODEL || options.defaultChatModel,\n  };\n}\n\nexport function readAiGatewayContractConfig(\n  env: AiGatewayEnvRecord,\n  options: AiGatewayContractOptions\n): AiGatewayContractConfig {\n  assertSupportedNodeRuntime();\n  const resolvedEnv = resolveAiGatewayContractEnv(env, options);\n\n  if (!resolvedEnv.apiKey) {\n    throw new Error(options.missingApiKeyError);\n  }\n\n  return {\n    apiKey: resolvedEnv.apiKey,\n    baseURL: resolvedEnv.baseURL,\n    chatModel: resolvedEnv.chatModel,\n  };\n}\n\nexport function buildAiGatewayContractSetupState<\n  TConfig extends AiGatewaySetupConfig,\n>(\n  env: AiGatewayEnvRecord,\n  options: AiGatewayContractOptions<TConfig>\n): AiGatewayContractSetupState<TConfig> {\n  const issues: string[] = [];\n  const resolvedEnv = resolveAiGatewayContractEnv(env, options);\n\n  try {\n    assertSupportedNodeRuntime();\n  } catch (error) {\n    issues.push(\n      error instanceof Error ? error.message : \"Unsupported Node.js runtime.\"\n    );\n  }\n\n  if (!resolvedEnv.apiKey) {\n    issues.push(options.missingApiKeyIssue || genericMissingApiKeyIssue);\n  }\n\n  issues.push(...(options.getAdditionalIssues?.(resolvedEnv, env) ?? []));\n\n  return {\n    config:\n      options.buildConfig?.(resolvedEnv, env) ??\n      ({\n        baseURL: resolvedEnv.baseURL,\n        chatModel: resolvedEnv.chatModel,\n      } as TConfig),\n    isReady: issues.length === 0,\n    issues,\n    nodeVersion: process.version,\n  };\n}\n\nexport function createAiGatewayFromContract(\n  env: AiGatewayEnvRecord,\n  options: AiGatewayContractOptions\n): ReturnType<typeof createGateway> {\n  const { apiKey, baseURL } = readAiGatewayContractConfig(env, options);\n\n  return createGateway({\n    apiKey,\n    baseURL,\n  });\n}\n",
      "type": "registry:lib",
      "target": "@lib/ai-gateway/contract.ts"
    },
    {
      "path": "registry/loop-agent/lib/loop-agent/env-source.ts",
      "content": "export function getLoopAgentAppEnv() {\n  // biome-ignore lint/style/noProcessEnv: Registry source installs into consumer apps without this repo's env wrapper.\n  return process.env;\n}\n",
      "type": "registry:lib",
      "target": "@lib/loop-agent/env-source.ts"
    },
    {
      "path": "registry/loop-agent/lib/loop-agent/env.ts",
      "content": "import { getLoopAgentAppEnv } from \"./env-source\";\nimport {\n  buildAiGatewayContractSetupState,\n  createAiGatewayFromContract,\n  readAiGatewayContractConfig,\n  type AiGatewayContractConfig,\n  type AiGatewayContractSetupState,\n  type AiGatewayEnvRecord,\n  type AiGatewaySetupConfig,\n} from \"@/lib/ai-gateway/contract\";\n\nexport const DEFAULT_LOOP_AGENT_CHAT_MODEL = \"openai/gpt-5-mini\";\n\nexport type LoopAgentEnv = AiGatewayEnvRecord;\n\nexport type LoopAgentConfig = AiGatewayContractConfig;\n\nexport type LoopAgentSetupState = AiGatewayContractSetupState<AiGatewaySetupConfig>;\n\nexport type LoopAgentGateway = ReturnType<typeof createAiGatewayFromContract>;\n\nconst loopAgentContract = {\n  defaultChatModel: DEFAULT_LOOP_AGENT_CHAT_MODEL,\n  missingApiKeyError: \"Missing AI_GATEWAY_API_KEY. Add it to .env.local before using the loop agent.\",\n  missingApiKeyIssue: \"AI_GATEWAY_API_KEY is missing. The demo can render, but chat requests will fail until it is configured.\",\n} as const;\n\nexport function getLoopAgentEnv(): LoopAgentEnv {\n  return getLoopAgentAppEnv();\n}\n\nexport function getLoopAgentConfig(\n  env: LoopAgentEnv = getLoopAgentEnv()\n): LoopAgentConfig {\n  return readAiGatewayContractConfig(env, loopAgentContract);\n}\n\nexport function getLoopAgentSetupState(\n  env: LoopAgentEnv = getLoopAgentEnv()\n): LoopAgentSetupState {\n  return buildAiGatewayContractSetupState(env, loopAgentContract);\n}\n\nexport function createLoopAgentGateway(\n  env: LoopAgentEnv = getLoopAgentEnv()\n): LoopAgentGateway {\n  return createAiGatewayFromContract(env, loopAgentContract);\n}\n",
      "type": "registry:lib",
      "target": "@lib/loop-agent/env.ts"
    },
    {
      "path": "registry/loop-agent/lib/loop-agent/model.ts",
      "content": "import {\n  DEFAULT_LOOP_AGENT_CHAT_MODEL,\n  getLoopAgentEnv,\n  type LoopAgentEnv,\n} from \"./env\";\n\nexport const LOOP_AGENT_PROVIDER_OPTIONS = {\n  openai: {\n    reasoningEffort: \"medium\",\n    reasoningSummary: \"auto\",\n  },\n} as const;\n\nfunction getModelName(modelId: string): string {\n  return modelId.split(\"/\").at(-1)?.toLowerCase() ?? modelId.toLowerCase();\n}\n\nexport function supportsLoopAgentReasoningOptions(modelId: string): boolean {\n  const modelName = getModelName(modelId);\n\n  return (\n    modelName.startsWith(\"gpt-5\") ||\n    modelName.startsWith(\"o1\") ||\n    modelName.startsWith(\"o3\") ||\n    modelName.startsWith(\"o4\")\n  );\n}\n\nexport function resolveLoopAgentProviderOptions(modelId: string) {\n  return supportsLoopAgentReasoningOptions(modelId)\n    ? LOOP_AGENT_PROVIDER_OPTIONS\n    : undefined;\n}\n\nexport function resolveLoopAgentChatModel(\n  env: LoopAgentEnv = getLoopAgentEnv()\n): string {\n  return env.AI_GATEWAY_CHAT_MODEL || DEFAULT_LOOP_AGENT_CHAT_MODEL;\n}\n",
      "type": "registry:lib",
      "target": "@lib/loop-agent/model.ts"
    },
    {
      "path": "registry/loop-agent/lib/loop-agent/runtime.ts",
      "content": "import { type UIMessage, validateUIMessages } from \"ai\";\n\nimport {\n  getLoopAgentEnv,\n  getLoopAgentSetupState,\n  type LoopAgentEnv,\n} from \"./env\";\nimport { streamLoopAgent } from \"./chat\";\nimport { resolveLoopAgentChatModel } from \"./model\";\n\ninterface LoopAgentRequestBody {\n  messages?: UIMessage[];\n}\n\ninterface LoopAgentRequestDependencies {\n  streamLoopAgent: (\n    messages: UIMessage[],\n    env: LoopAgentEnv\n  ) => Promise<Response> | Response;\n}\n\nexport interface LoopAgentRuntimeState {\n  chatModel: string;\n  isChatAvailable: boolean;\n  nodeVersion: string;\n  setupMessage: string | null;\n  statusLabel: \"Ready\" | \"Setup required\";\n}\n\nconst invalidMessagesError = 'Expected a JSON body with a \"messages\" array.';\nconst invalidUiMessagesError =\n  'Expected each \"messages\" entry to match the UIMessage format.';\n\nasync function readLoopAgentMessages(body: unknown): Promise<UIMessage[]> {\n  const { messages } = (body ?? {}) as LoopAgentRequestBody;\n\n  if (!Array.isArray(messages)) {\n    throw new Error(invalidMessagesError);\n  }\n\n  try {\n    return await validateUIMessages({ messages });\n  } catch {\n    throw new Error(invalidUiMessagesError);\n  }\n}\n\nexport function getLoopAgentRuntimeState(\n  env: LoopAgentEnv = getLoopAgentEnv()\n): LoopAgentRuntimeState {\n  const setup = getLoopAgentSetupState(env);\n\n  return {\n    chatModel: resolveLoopAgentChatModel(env),\n    isChatAvailable: setup.isReady,\n    nodeVersion: setup.nodeVersion,\n    setupMessage: setup.issues.length > 0 ? setup.issues.join(\" \") : null,\n    statusLabel: setup.isReady ? \"Ready\" : \"Setup required\",\n  };\n}\n\nexport async function handleLoopAgentRequest(\n  request: Request,\n  env: LoopAgentEnv = getLoopAgentEnv(),\n  dependencies: LoopAgentRequestDependencies = {\n    streamLoopAgent,\n  }\n) {\n  const runtimeState = getLoopAgentRuntimeState(env);\n\n  if (!runtimeState.isChatAvailable) {\n    return Response.json(\n      {\n        error: runtimeState.setupMessage,\n      },\n      { status: 500 }\n    );\n  }\n\n  try {\n    const messages = await readLoopAgentMessages(await request.json());\n\n    return dependencies.streamLoopAgent(messages, env);\n  } catch (error) {\n    if (\n      error instanceof Error &&\n      [invalidMessagesError, invalidUiMessagesError].includes(error.message)\n    ) {\n      return Response.json(\n        {\n          error: error.message,\n        },\n        { status: 400 }\n      );\n    }\n\n    throw error;\n  }\n}\n",
      "type": "registry:lib",
      "target": "@lib/loop-agent/runtime.ts"
    },
    {
      "path": "registry/loop-agent/lib/loop-agent/support-triage.ts",
      "content": "export type SupportTriageAction = \"escalate\" | \"monitor\" | \"reply\";\n\nexport type SupportTriageExecution = \"parallel\" | \"sequential\";\n\nexport type SupportTriagePriority = \"high\" | \"low\" | \"medium\";\n\nexport interface SupportTriageCustomer {\n  id: string;\n  name: string;\n  plan: string;\n}\n\nexport interface SupportTriageRecommendation {\n  action: SupportTriageAction;\n  priority: SupportTriagePriority;\n  rationale: string[];\n}\n\nexport interface SupportTriageApprovalRequest {\n  action: \"escalate\";\n  caseId: string;\n  customerName: string;\n  customerUpdate: string;\n  internalHandoff: string;\n  priority: \"high\";\n  rationale: string[];\n}\n\nexport interface SupportTriageApprovalResult\n  extends SupportTriageApprovalRequest {\n  approvalStatus: \"approved\";\n  handoffChannel: \"priority escalation\";\n  nextStep: string;\n}\n\nexport interface SupportTriageResult {\n  caseId: string;\n  customer: SupportTriageCustomer;\n  recommendation: SupportTriageRecommendation;\n  risk: SupportTriageRisk;\n  ticket: SupportTriageTicket;\n  toolBatches: SupportTriageToolBatch[];\n}\n\nexport interface SupportTriageRisk {\n  level: SupportTriagePriority;\n  minutesRemaining: number;\n  reason: string;\n}\n\nexport interface SupportTriageTicket {\n  id: string;\n  severity: \"normal\" | \"urgent\";\n  title: string;\n}\n\nexport interface SupportTriageToolBatch {\n  execution: SupportTriageExecution;\n  label: string;\n  tools: string[];\n}\n\nexport function runSupportTriageLoop(): SupportTriageResult {\n  return {\n    caseId: \"CASE-1842\",\n    customer: {\n      id: \"cus_northstar\",\n      name: \"Northstar Analytics\",\n      plan: \"Enterprise\",\n    },\n    recommendation: {\n      action: \"escalate\",\n      priority: \"high\",\n      rationale: [\n        \"Enterprise customer has an active entitlement.\",\n        \"The active ticket is close to the response SLA.\",\n      ],\n    },\n    risk: {\n      level: \"high\",\n      minutesRemaining: 18,\n      reason: \"Urgent enterprise ticket is inside the 30 minute SLA window.\",\n    },\n    ticket: {\n      id: \"TIC-7789\",\n      severity: \"urgent\",\n      title: \"Production dashboard exports are timing out\",\n    },\n    toolBatches: [\n      {\n        execution: \"parallel\",\n        label: \"Independent account context\",\n        tools: [\"getCustomerProfile\", \"listRecentTickets\", \"checkEntitlement\"],\n      },\n      {\n        execution: \"sequential\",\n        label: \"Dependent SLA decision\",\n        tools: [\"calculateSlaRisk\"],\n      },\n      {\n        execution: \"sequential\",\n        label: \"Human approval checkpoint\",\n        tools: [\"requestHumanApproval\"],\n      },\n    ],\n  };\n}\n\nexport function buildSupportEscalationApprovalRequest(\n  triage: SupportTriageResult = runSupportTriageLoop()\n): SupportTriageApprovalRequest {\n  if (\n    triage.recommendation.action !== \"escalate\" ||\n    triage.recommendation.priority !== \"high\"\n  ) {\n    throw new Error(\"Support escalation approval requires a high escalation.\");\n  }\n\n  return {\n    action: \"escalate\",\n    caseId: triage.caseId,\n    customerName: triage.customer.name,\n    customerUpdate:\n      \"We are escalating your export timeout incident to priority support because the SLA window is at risk.\",\n    internalHandoff: `Route ${triage.ticket.id} to priority support with ${triage.risk.minutesRemaining} minutes remaining in the response SLA.`,\n    priority: \"high\",\n    rationale: triage.recommendation.rationale,\n  };\n}\n\nexport function recordSupportEscalationApproval(\n  approvalRequest: SupportTriageApprovalRequest\n): SupportTriageApprovalResult {\n  return {\n    ...approvalRequest,\n    approvalStatus: \"approved\",\n    handoffChannel: \"priority escalation\",\n    nextStep:\n      \"Open the priority escalation, send the customer update, and attach the SLA risk summary.\",\n  };\n}\n",
      "type": "registry:lib",
      "target": "@lib/loop-agent/support-triage.ts"
    }
  ],
  "envVars": {
    "AI_GATEWAY_API_KEY": "",
    "AI_GATEWAY_BASE_URL": "https://ai-gateway.vercel.sh/v3/ai",
    "AI_GATEWAY_CHAT_MODEL": "openai/gpt-5-mini"
  },
  "docs": "Install into a Next.js App Router project initialized with shadcn/ui. This item adds the loop-agent page, chat route, approval-aware chat workspace, support triage tools, and AI Gateway env vars required for the bounded tool loop.",
  "type": "registry:block"
}
