# The tutorials moved faster than I did: building an agent that catches deprecated dependencies

> Source: <https://dev.to/joshyfruit/the-tutorials-moved-faster-than-i-did-building-an-agent-that-catches-deprecated-dependencies-eh0>
> Published: 2026-09-03 06:08:52+00:00

I set out to learn Amazon Bedrock AgentCore, AWS’s managed runtime for AI agents. So the plan is to find a few tutorials, follow one, and deploy something.

Instead, I found the project I ended up building.

Several walkthroughs teach `bedrock-agentcore-starter-toolkit`

. The package now describes itself this way:

Python CLI toolkit for Amazon Bedrock AgentCore

(legacy). For new projects, use the AgentCore CLI.

No one did anything wrong. AWS released a replacement, `@aws/agentcore`

, and clearly marked the old package as legacy. The walkthroughs were accurate when their authors published them. The tooling progressed more quickly, which happens often in this ecosystem.

Some current AWS guides teach the new CLI. The stale material includes the old toolkit’s own docs site, which is still online and easy to reach, plus third-party posts. No single source is wrong, so there is no single fix. You have to check what you are installing.

So I found a use case. `strands-agents/sdk-python`

, the agent framework’s repository, had been renamed to `strands-agents/harness-sdk`

. *hmmmm! 🤔*.

You can see the redirect here:

The response is **301 Moved Permanently**. Follow it, and you land on `harness-sdk`

. Nothing breaks. Old links still resolve because that is what a 301 is for. `pip install strands-agents`

also works because the package name did not change, only the repository did. The PyPI listing already points to the new URL. GitHub and PyPI are both doing the helpful thing.

That is why the rename is easy to miss. Every signal you would normally check looks fine.

I caught both cases only because I checked the repositories instead of trusting what I had read. If I had followed a tutorial as written, I would have installed a legacy CLI and spent an hour wondering why a command didn't exist. That gave me something worth building.

Tools already exist for outdated dependencies: Dependabot, Renovate, `npm outdated`

, and `pip list --outdated`

. They compare the version you pinned with the latest version, then report the difference.

That works well for version gaps, but it misses three other problems:

| What happened | What the version number shows |
|---|---|
The repo was renamed
|
Nothing. Your old name still resolves. |
The project was archived
|
Nothing. The last version is still the latest version. |
The notes say “deprecated, migrate to X”
|
Nothing. That’s prose, in a release body. |

Number comparison cannot detect any of these. The evidence is written in plain English on the repository page. In the third case, someone has written a paragraph asking you to stop using the dependency.

Number comparison cannot read. An agent can. “Read this page and tell me if it says anything alarming” is a good task for a language model and a poor one for a regex.

I called the project **Release Radar**.

You give it a list of pinned dependencies. For each one, it finds the repository, reads its description and latest release notes, checks the version distance, and returns one of three verdicts:

That is the whole product. I kept it small enough to finish in an afternoon, but useful enough to catch a real problem.

Two parts of the stack are easy to confuse:

Just to let you know, this is a 2-part series;

1️⃣ This activity (this post you are reading) builds the agent and checks its deterministic function logic locally.

2️⃣ The next post moves the complete agent into AgentCore.

First, remove the conflicting CLI:

```
pip uninstall bedrock-agentcore-starter-toolkit
# Or depending on how you installed it: 
# pipx uninstall
# uv tool uninstall

npm install -g @aws/agentcore #install agentcore

which -a agentcore  # Must print exactly ONE path.
```

Do not skip `which -a`

. If it prints two paths, the old CLI is shadowing the new one. Every confusing error that follows will trace back to that conflict.

Next, scaffold the project. The wizard asks for a framework and a model provider. Choose **Strands Agents** and **Amazon Bedrock**. Bedrock is the only provider in the wizard that does not require an API key because it uses your existing AWS credentials. Anthropic, Google Gemini, and OpenAI each require a key.

```
agentcore create  # Name it: release-radar
cd release-radar
```

The command creates two directories: configuration in `agentcore/`

and your code in `app/`

. You only need to edit `app/`

.

A Strands tool is a regular Python function with a decorator. Its docstring is routing logic, not ordinary documentation.

The model reads the docstring to decide whether to call the function. If it is vague, the tool may never run, leaving you with a correct-looking agent that ignores half its capabilities. Write docstrings for the model rather than for a future maintainer.

The second paragraph below does most of the routing work:

``` python
import json
import os
import urllib.error
import urllib.request

from strands import tool

API = "https://api.github.com"

def _get(path: str) -> dict:
    """GET a GitHub API path. Never raises—errors come back as data."""
    req = urllib.request.Request(
        API + path,
        headers={
            "Accept": "application/vnd.github+json",
            "User-Agent": "release-radar",
        },
    )

    # Lifts the anonymous rate limit from 60/hr to 5000/hr.
    if token := os.environ.get("GITHUB_TOKEN"):
        req.add_header("Authorization", f"Bearer {token}")

    try:
        with urllib.request.urlopen(req, timeout=10) as r:
            return json.load(r)
    except urllib.error.HTTPError as e:
        return {"_error": f"HTTP {e.code}"}
    except Exception as e:
        return {"_error": str(e)[:120]}

@tool
def repo_status(owner: str, repo: str) -> dict:
    """Look up a GitHub repository's current identity and health.

    Returns the canonical full_name, which differs from the requested
    owner/repo when the project has been renamed. Also reports whether
    the repo is archived and when it was last pushed to. Call this
    FIRST for every dependency—a rename or archive matters more than
    any version gap.
    """
    asked = f"{owner}/{repo}"
    d = _get(f"/repos/{owner}/{repo}")
    if "_error" in d:
        return {"requested": asked, "error": d["_error"]}

    return {
        "requested": asked,
        "canonical": d["full_name"],
        "renamed": d["full_name"].lower() != asked.lower(),
        "archived": d["archived"],
        "description": (d["description"] or "")[:280],
        "last_push": d["pushed_at"],
        "stars": d["stargazers_count"],
    }
```

`_get`

never raises. It returns errors as `{"_error": ...}`

data. When a tool throws, it kills the agent’s turn. When it returns an error string, the model can say “that one 404’d” and continue. The failure becomes data instead of control flow.

So the rename check is one line: `d["full_name"].lower() != asked.lower()`

. GitHub’s API follows the redirect and returns the canonical name. Ask for `strands-agents/sdk-python`

, and it returns `strands-agents/harness-sdk`

. Comparing the two reveals the rename. That one line is why this project exists.

The second tool fetches release notes. Together with the description returned by `repo_status`

, this gives the model prose to inspect for words such as *deprecated*, *legacy*, *superseded*, and *migrate*:

``` php
@tool
def latest_release(owner: str, repo: str) -> dict:
    """Fetch the most recent published release for a repository.

    Returns the tag name, publish date, and the opening of the release
    notes. Read the notes for deprecation and breaking-change language.
    """
    d = _get(f"/repos/{owner}/{repo}/releases/latest")
    if "_error" in d:
        return {
            "repo": f"{owner}/{repo}",
            "error": d["_error"],
            "hint": "404 here usually means the repo publishes tags, not releases",
        }

    return {
        "repo": f"{owner}/{repo}",
        "tag": d["tag_name"],
        "published": d["published_at"],
        "notes": (d.get("body") or "")[:600],
    }
```

This field `hint`

is intentional because the model reads the error when the tool fails, so the error should explain what probably happened. Many repositories publish tags without releases. A bare **404** would leave the agent guessing.

*One limitation, GitHub defines “latest” at the repository level. In a monorepo with separate Python and TypeScript release streams, this endpoint may return the newest release for the wrong language. That does not affect a rename or archive verdict, but production code should filter releases by the dependency’s tag prefix.*

The third tool compares versions. Most of the code is parsing because version tags come in forms such as `1.2.3`

, `v1.2.3`

, and, in Strands’ case, `python/v1.54.0`

:

``` php
@tool
def version_gap(pinned: str, latest: str) -> dict:
    """Compare a pinned version against the latest release tag.

    Handles common prefixes (v1.2.3, python/v1.2.3) and returns how many
    major, minor, and patch releases the pin is behind.
    """

    def parts(v: str) -> tuple:
        tail = v.strip().rsplit("/", 1)[-1].lstrip("vV")
        core = tail.split("-")[0].split("+")[0]
        out = []
        for chunk in core.split(".")[:3]:
            digits = "".join(c for c in chunk if c.isdigit())
            out.append(int(digits) if digits else 0)
        while len(out) < 3:
            out.append(0)
        return tuple(out)

    p, l = parts(pinned), parts(latest)
    level = ("major", "minor", "patch")
    behind = None
    for i in range(3):
        if l[i] != p[i]:
            behind = level[i] if l[i] > p[i] else None
            break

    return {
        "pinned": pinned,
        "latest": latest,
        "behind_by": behind,
        "current": p >= l,
    }
```

My first attempt at the comparison loop was wrong. Because this function contains the project’s only substantial logic, I added asserts:

```
if __name__ == "__main__":
    assert version_gap("1.0.0", "1.0.0")["current"] is True
    assert version_gap("v0.1.0", "v0.28.1")["behind_by"] == "minor"
    assert version_gap("1.2.3", "2.0.0")["behind_by"] == "major"
    assert version_gap("python/v1.53.0", "python/v1.54.0")["behind_by"] == "minor"
    assert version_gap("2.0.0", "1.9.9")["behind_by"] is None
    print("version_gap ok")
```

The last assert failed.

My first version found the earliest position where the latest version exceeded the pinned version:

```
behind = next((level[i] for i in range(3) if l[i] > p[i]), None)
```

Try that with `2.0.0`

and `1.9.9`

. At the major position, `1 > 2`

is false. At the minor position, `9 > 0`

is true. The function therefore reports “minor behind” even though the dependency is a full major version ahead.

The code scans all three positions independently, but version comparison must move from left to right and stop at the first difference. Later positions no longer matter. The corrected `for`

loop does that with a `break`

.

I could have written the one-liner, decided it looked right, and shipped it. You would only discover the bug when the agent confidently told you to upgrade a dependency you had already upgraded. At that point, you might blame the model instead of the arithmetic.

Five asserts caught the bug in thirty seconds. The deterministic parts of an agent still need tests. A model in the loop makes some behavior fuzzy, but `version_gap`

is arithmetic, and arithmetic is easy to check.

Run the checks before continuing:

```
uv run python tools.py  # → version_gap ok
python
from strands import Agent

from tools import latest_release, repo_status, version_gap

SYSTEM = """You audit pinned software dependencies.

For each `owner/repo@version` the user gives you:

 1. Call repo_status first. If `renamed` is true or `archived` is
    true, that is the headline; report it before anything else.
 2. Read the repository description and latest release notes for
    deprecated, legacy, superseded, migrate, or breaking.
 3. Call version_gap to measure the distance.

Then assign exactly one verdict per dependency:

 BLOCKED  renamed, archived, or description/notes say deprecated/legacy
 BEHIND   major or minor releases behind
 OK       current, or patch-behind only

Output one line per dependency: VERDICT  owner/repo  one-clause reason.

No preamble. If a tool returns an error, say so and move on."""

agent = Agent(
    system_prompt=SYSTEM,
    tools=[repo_status, latest_release, version_gap],
)
```

The prompt sets the tool order, verdict vocabulary, and output format. This will leave those choices open, and the model tends to return three paragraphs of friendly hedging. A line such as `BLOCKED aws/foo - archived`

is easy to `grep`

and act on later.

`agentcore create`

will generate a richer `main.py`

than the version above. It will include session caching, MCP wiring, and a two-argument `invoke(payload, context)`

entry point imported from `bedrock_agentcore.runtime`

. The shorter version is easier to learn from, but in practice you should modify the generated file rather than replace it. Add `tools.py`

, add one import, swap the system prompt, and replace the demo tool. That is three edits.

Now test the three dependencies from the start of this post:

```
Audit these:

aws/bedrock-agentcore-starter-toolkit@0.1.0
strands-agents/sdk-python@1.0.0
aws/agentcore-cli@0.28.1
```

The last pin was current when I wrote this. Run `npm view @aws/agentcore version`

to get today’s version. If it has moved, you will get a live **BEHIND** result alongside the two **BLOCKED** results.

This is what the agent returned:

The response may vary from time to time since LLMs are non-deterministic in nature. Across three runs, the third result said “pinned version is current,” “pinned version matches latest release,” and “current at v0.28.1.” What matters is that the first two results are **BLOCKED** and their reasons identify the rename and legacy status. If either comes back **OK**, `repo_status`

is probably not being called. Check that its docstring survived the copy.

The agent finds the same problem that prompted me to build it. These are two live repositories, not synthetic fixtures, and either could cost someone an afternoon.

That is my standard for a tutorial project. It should do more than return “hello” from a hello-world fixture. It should fail informatively when pointed at reality.

The tools and agent wiring are ready. My 2nd post will cover deploying our Agent to AgentCore and will cover the issues that appear after it leaves your computer 🖥️:

I have run these examples against the live GitHub API, npm, and PyPI. I also deployed the agent to a real AgentCore runtime, invoked it three times, and tore it down. *The screenshot above is output from that deployed runtime, not a mocked fixture.*

This post is a snapshot, and snapshots decay. If you are reading it much later, run the agent against its own examples. If `strands-agents/sdk-python`

no longer returns a 301, something moved again. That is more useful to know than whether this post still looks trustworthy.

Here’s my [GitHub repo](https://github.com/coozgan/release-radar.git) for the code used in this project.
