cd /news/ai-agents/the-tutorials-moved-faster-than-i-di… · home topics ai-agents article
[ARTICLE · art-119895] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=· neutral

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

A developer built Release Radar, an AI agent that detects deprecated dependencies by reading repository descriptions and release notes, after discovering that tutorials for Amazon Bedrock AgentCore's legacy toolkit were outdated. The agent catches issues like repository renames, archived projects, and deprecation notices that version-number comparison tools miss.

read11 min views2 publishedSep 3, 2026

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

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:

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",
        },
    )

    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:

@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

:

@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 for the code used in this project.

── more in #ai-agents 4 stories · sorted by recency
── more on @amazon bedrock agentcore 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/the-tutorials-moved-…] indexed:0 read:11min 2026-09-03 ·