Workspace/Lesson workspace
Loading progress
Foundations40 min

A minimal agent UI and API that tell the truth

Lesson 3 of 3
How to study this lesson

Read one section, trace the worked example, and try the knowledge check. Bookmark saves a shortcut; notes appear in My notebook. Mark lesson complete is your own assessment and does not mark its lab passed.

By the end, you can

  • Divide responsibilities among browser, API, controller, and tools.
  • Design run endpoints with ownership and lifecycle semantics.
  • Render evidence and partial status without leaking internal authority.

Keep the first architecture small and explicit

A minimal application has four responsibilities: a browser presents inputs and progress, an API validates requests and authenticates users, a controller manages run state and budgets, and tool adapters perform allowed operations. These can initially live in one backend process with clear function boundaries. Separate responsibilities do not require a separate microservice for each one.

The browser sends a small JSON request and receives an application envelope. The controller should be callable without HTTP so a command-line demo and tests use the same logic. This prevents the UI from becoming the only place where a budget or completion rule exists. A malicious or buggy client can bypass frontend controls, so server-side validation remains the authority for execution.

Choose synchronous or asynchronous semantics

For a short, bounded lookup, POST /runs can execute immediately and return a completed result or a controlled failure. For longer work, it can return an accepted response with a run ID and status, followed by GET /runs/{id} for progress. A cancellation endpoint expresses user intent while the controller determines which work can still be stopped. Document the chosen semantics rather than mixing both behaviors unpredictably.

Every run belongs to a user or authorized scope. Knowing a run ID must not be enough to read someone else's question or tool output. The API checks ownership on creation, retrieval, streaming, and cancellation. The teaching seed omits authentication to remain locally runnable, and its README explicitly limits it to a localhost demonstration. Adding deployment requires implementing those missing boundaries.

Show states and evidence in the interface

Useful progress describes observable work: validating request, looking up A7, checking result, completed. It need not expose hidden model reasoning. Show a concise action trace, relevant evidence, and the final result. If the model emits draft text during processing, label it as partial until the run's terminal checks pass. A spinner alone does not explain whether a tool is waiting, retrying, or blocked.

For our catalog example, the final view can show SKU A7, eight available units, and the source call identifier. For a missing SKU, show not found rather than zero stock; those are different facts. For failure, preserve a safe error category and a retry action where appropriate. This information helps users decide what to do next instead of interpreting a generic success-colored card.

Treat rendered content as untrusted data

User questions, model text, and tool results can contain characters that are meaningful to HTML or JavaScript. Render ordinary text through safe text APIs or escape it before inserting into generated HTML. Do not treat a model's output as executable markup or use it to choose arbitrary client-side code. Similarly, a returned tool instruction should not expand server permissions.

A clean API response contains the facts needed for the interface and omits credentials, internal stack traces, and unrelated records. Error details intended for operators can be stored behind appropriate access controls. Limit request sizes and validate content types before parsing. These boundaries make the product more reliable as well as safer: a malformed input should become a clear error rather than a broken page or an accidental action.

Build the portfolio around observable behavior

The lesson example creates an HTML view from a run envelope using escaping, proving that a question containing script-like text remains text. The mini project provides a standard-library localhost server with a small form and a JSON endpoint, plus a deterministic command-line demonstration. It is a seed for learning the architecture, not a deployment-ready web service.

Complete the project by adding state-aware progress, ownership checks, contract tests, and a model adapter behind the existing controller boundary. Record a successful lookup, a missing item, a rejected request, and a budget failure. Explain which parts are deterministic and which depend on the model. A reviewer should be able to run the system, inspect its evidence, and see its limits without guessing how an attractive screenshot was produced.

Work through the code

The program safely renders a simple completed run into an HTML string and checks that script-like input is escaped. It performs no file writes or network calls. This is a narrow server-rendering example; a browser framework should use its normal safe text rendering and avoid raw HTML insertion for model content.

m06_lesson_3.py
python
from html import escape
import json

def render_run(run):
    status = escape(run["status"])
    question = escape(run["question"])
    if run["status"] == "completed":
        result = escape(json.dumps(run["result"], sort_keys=True))
        detail = f"<pre>{result}</pre>"
    else:
        detail = "<p>Result is not complete.</p>"
    return f"<article><h2>{status}</h2><p>{question}</p>{detail}</article>"

run = {"status": "completed", "question": "Check <script>alert(1)</script>",
       "result": {"sku": "A7", "available": 8}}
html = render_run(run)
print("raw script tag present:", "<script>" in html)
print("escaped script text present:", "&lt;script&gt;" in html)
print("completed view:", "<pre>" in html)
EXPECTED / ILLUSTRATIVE OUTPUT
raw script tag present: False
escaped script text present: True
completed view: True

Run Python snippets locally with the prerequisites named above. The module coding lab runs directly in your browser.

Pause and reason

Your frontend disables the Run button after one click, but a client sends the same POST twice directly. Where should duplicate-run policy live, and how does it differ from tool idempotency?

Check your understanding

A completed response says found=false and available=null. How should the UI present it?

Your notes

Explain the mechanism in your own words. Add a failure you want to test.

Saved notes appear in your notebook

Go deeper with primary sources

Practice this module