Back to guides
5
5 min

Agent App

From CLI to Web — Building Trust Through Visibility

Why the UI Matters

Your agent works. It plans, calls tools, handles errors, and respects guardrails. But if users can't *see* what it's doing, they won't trust it. An agent behind a chat box feels like a black box. An agent that shows its work — what tools it's calling, what data it found, why it made a decision — earns trust incrementally.

The UI isn't decoration. It's the trust layer.

Streaming Tool Execution

When a user asks "Prep me for the Globex call," the agent might take 10-15 seconds across multiple tool calls. A loading spinner for 15 seconds feels broken. Streaming the agent's process feels collaborative:

User: "Prep me for the Globex renewal call tomorrow."

  ┌─────────────────────────────────────────┐
  │ 🔍 Searching CRM for "Globex"...       │
  │    Found: Globex Corp — 3 contacts      │
  │                                          │
  │ 📊 Pulling deal history...               │
  │    $240K ARR, renewal in 2 days          │
  │    Open support ticket: latency issues   │
  │                                          │
  │ 📧 Checking recent emails...             │
  │    Last contact: 5 days ago (Jane Smith) │
  │                                          │
  │ ✅ Generating call prep brief...         │
  └─────────────────────────────────────────┘

Each tool call streams to the UI as it happens. The user watches the agent work and can intervene if it's going down the wrong path ("That's the wrong Globex — I mean the one in Chicago").

Implementation Pattern

Use Server-Sent Events (SSE) to stream tool execution steps to the client:

// Server: stream agent steps
function streamAgentExecution(res: Response, agentStream: AsyncIterable<AgentEvent>) {
  const encoder = new TextEncoder();

  const stream = new ReadableStream({
    async start(controller) {
      for await (const event of agentStream) {
        if (event.type === "tool_start") {
          controller.enqueue(encoder.encode(
            `data: ${JSON.stringify({ type: "tool_start", tool: event.name, args: event.args })}\n\n`
          ));
        } else if (event.type === "tool_result") {
          controller.enqueue(encoder.encode(
            `data: ${JSON.stringify({ type: "tool_result", tool: event.name, result: event.result })}\n\n`
          ));
        } else if (event.type === "text_delta") {
          controller.enqueue(encoder.encode(
            `data: ${JSON.stringify({ type: "text", content: event.content })}\n\n`
          ));
        }
      }
      controller.enqueue(encoder.encode("data: [DONE]\n\n"));
      controller.close();
    },
  });

  return new Response(stream, {
    headers: { "Content-Type": "text/event-stream" },
  });
}

Approval UX Patterns

When the agent needs human approval (Module 4), the UX determines adoption. Get this wrong and users either skip approvals (dangerous) or abandon the agent (wasteful).

Inline Approval — Not Modal Dialogs

Approvals should appear inline in the conversation flow, not as intrusive modal dialogs:

Agent: I'd like to send this email to Jane Smith at Globex:

  ┌─────────────────────────────────────────┐
  │ 📧 Send Email                           │
  │                                          │
  │ To: jane.smith@globex.com               │
  │ Subject: Renewal Proposal — Acme Corp   │
  │ Body: Hi Jane, following up on our...   │
  │                                          │
  │ [Preview Full Email]                     │
  │                                          │
  │ [✓ Approve & Send]  [✗ Reject]  [Edit] │
  └─────────────────────────────────────────┘

Three response options:

  • Approve — Execute as planned
  • Reject — Cancel with optional reason ("Don't email her directly, CC her manager")
  • Edit — Modify the action before executing (change subject line, add CC, adjust body)
  • The rejection reason feeds back into the agent's state so it can replan. This is the human-in-the-loop *learning* moment — the agent gets better at predicting what you'll approve.

    Conversation Memory

    Agents need context across turns. A user who says "What about their competitor?" expects the agent to know which account they were just discussing. This requires conversation memory:

    Short-term Memory (Session)

    The current conversation's message history. Include tool calls and results — the model needs to know what it already looked up:

    interface ConversationMessage {
      role: "user" | "assistant" | "tool";
      content: string;
      tool_name?: string;       // For tool messages: which tool produced this
      tool_call_id?: string;    // Links tool results back to the request
      timestamp: string;
    }

    Long-term Memory (Cross-session)

    Facts the agent learned about this user across conversations. Store as key-value pairs, not raw chat logs:

    interface UserMemory {
      preferred_accounts: string[];    // Accounts they work with most
      communication_style: string;     // "Keep it brief" vs "Give me detail"
      role: string;                    // "Enterprise AE" — affects what tools are relevant
      last_topics: string[];           // What they asked about recently
    }

    Context Window Management

    With multiple tool results, context fills up fast. Prioritize:

  • System prompt (always)
  • User memory (always)
  • Current turn's tool results (always)
  • Recent messages (last 10-15)
  • Earlier messages (summarized)
  • Source Attribution

    When the agent answers a question, users need to know *where* the answer came from. "Your ARR is $240K" is a claim. "Your ARR is $240K (source: CRM deal #4521, updated 2 days ago)" is a verifiable fact.

    Agent: Based on your CRM data, here's the Globex summary:
    
      • ARR: $240,000 ← CRM Deal #4521
      • Contract end: June 15, 2026 ← CRM Deal #4521
      • Open tickets: 1 (latency in EU region) ← Support Ticket #8834
      • Last contact: May 28 via email ← Gmail search
    
      [View sources]

    Each fact links back to the tool call that produced it. This builds trust and lets users verify critical data before acting on it.

    Error States

    Tools fail. The UI needs to handle this gracefully, not with a generic "Something went wrong":

    ScenarioWhat to ShowWhat to Offer
    Tool timeout"CRM search timed out after 10s"[Retry] or "I can check the cached data instead"
    Auth expired"Your CRM session expired"[Re-authenticate] link
    No results"No contacts found matching 'Globex'""Try a different spelling or search by email?"
    Rate limited"CRM API rate limit reached""I'll retry in 30 seconds" (auto-retry with countdown)
    Partial failure"Got deal history but email search failed"Show what succeeded, note what's missing

    Never hide failures. Users who discover their agent silently skipped a data source lose trust permanently. Show what worked, what didn't, and what the agent is doing about it.

    Putting It Together

    Your capstone app will combine all of these patterns: a streaming chat interface that shows tool execution in real-time, inline approval cards for high-risk actions, source attribution on every claim, and clear error handling. The result is an agent that feels less like a chatbot and more like a capable, transparent colleague.

    This is chapter 5 of Production AI Agents.

    Get the full hands-on course for $100 and build the complete system. Your projects become your portfolio.

    View course details