Labs / Lab 02

Wrap the CLI in a stable JSON interface

Turn loose text into one boring envelope so callers can trust the result without guessing.

What you'll build

A JSON wrapper around the term counter.

By the end of this lab you will have a command that counts a term in one or more files and always returns JSON. On success it includes the matched files and totals. On failure it still returns JSON, just with ok flipped and an error record in the envelope.

That may sound small, but it is one of the main moves in the whole stack. A human can read almost anything. A tool, protocol adapter, or agent needs output it can parse reliably every single time.

Run it

cd ai_ecosystem_labs
python3 02-json-wrapper/term_count_json.py agent sample_docs/agents.txt
Starting here? Quick setup
git clone https://github.com/BanditF/ai_ecosystem_labs
cd ai_ecosystem_labs
python3 02-json-wrapper/term_count_json.py agent sample_docs/agents.txt

Requires Python 3.8+. No additional packages needed for this lab.

Time guide. Setup: ~2 min. Working through it: 15–25 min, mostly around output shape and error handling.

Why this piece exists

Automation gets easier when success and failure share one shape.

If a script prints plain text on success and crashes or dumps traceback text on failure, every caller has to guess what happened. That means custom glue, brittle parsing, and a surprising amount of defensive code around something that should be simple.

A JSON envelope fixes that. The caller checks ok, reads items, summary, and errors, and moves on. Real APIs work the same way. They do not expect you to scrape random strings from stderr. They hand back structured data the next layer can make decisions from.

The code

term_count_json.py

Walk through it

Four things worth noticing.

The envelope shape stays stable

The important part is not just that the script returns JSON. It returns the same top-level idea every time: ok, items, summary, and errors, plus the extra context in tool and input. Callers do not have to guess what kind of response they got before they start reading it.

Errors are data, not a parsing problem

When a file is missing, the wrapper records {"file": name, "error": "missing_file"} inside errors and sets ok to false. That is much easier to handle than a crash. The caller can branch on the JSON instead of trying to interpret stderr text.

Schema consistency matters more than cleverness

Success returns items, summary, and an empty errors list. Failure returns the same fields, just with different values. That consistency is the whole win. Tests, wrappers, and hosts can keep one code path instead of special-casing every outcome.

JSON makes the tool composable

Once stdout is JSON, you can pretty-print it with python3 -m json.tool, filter it with jq, log it, replay it, or hand it straight to another tool. The wrapper stops being just a script and starts being a clean boundary other systems can build on.

Expected output

What a successful run looks like.

Success case:

{
  "ok": true,
  "tool": "term_count",
  "input": {
    "term": "agent",
    "files": [
      "sample_docs/agents.txt"
    ]
  },
  "items": [
    {
      "file": "sample_docs/agents.txt",
      "count": 2
    }
  ],
  "summary": {
    "total": 2
  },
  "errors": []
}

Failure case with a bad path:

{
  "ok": false,
  "tool": "term_count",
  "input": {
    "term": "agent",
    "files": [
      "sample_docs/does-not-exist.txt"
    ]
  },
  "items": [],
  "summary": {
    "total": 0
  },
  "errors": [
    {
      "file": "sample_docs/does-not-exist.txt",
      "error": "missing_file"
    }
  ]
}

The fields worth noticing are ok, which tells the caller whether the run succeeded, items, which holds per-file results, summary, which gives the total count, and errors, which stays present even when it is empty. That last part is what keeps the schema easy to consume.

Try this

Three things to try before moving on.

  1. Pretty-print the JSON. Run python3 02-json-wrapper/term_count_json.py agent sample_docs/agents.txt | python3 -m json.tool. The data does not change, only the presentation. That is a good reminder that once the shape is JSON, different tools can render it however they want.
  2. Pass a nonexistent file. Run python3 02-json-wrapper/term_count_json.py agent sample_docs/does-not-exist.txt. Notice that the command returns a JSON error envelope instead of a crash. This is the difference between something a person can inspect and something another tool can safely depend on.
  3. Add one more field and keep the shape consistent. The script already has a version field, so try adding a different stable field such as timestamp or schema_version near the top-level envelope. Then make sure it appears in both the success path and the --dry-run path so callers still get one predictable schema.

What you just built

A boundary other tools can trust.

You took a useful little script and made its output dependable. That is the real upgrade here. The counting logic matters less than the fact that callers now get a stable envelope they can inspect without guessing.

In practice, this is how a lot of AI tooling grows up. A quick script becomes a wrapper, the wrapper becomes a contract, and that contract becomes something protocols and agents can safely build around.

Concepts behind this

Read Protocols for the next step up. This kind of stable envelope is exactly what protocol adapters receive and normalize.

Read Agents for the orchestration layer. Agents depend on envelopes like this to decide what happened and what to do next.