{"slug": "the-tutorials-moved-faster-than-i-did-building-an-agent-that-catches-deprecated", "title": "The tutorials moved faster than I did: building an agent that catches deprecated dependencies", "summary": "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.", "body_md": "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.\n\nInstead, I found the project I ended up building.\n\nSeveral walkthroughs teach `bedrock-agentcore-starter-toolkit`\n\n. The package now describes itself this way:\n\nPython CLI toolkit for Amazon Bedrock AgentCore\n\n(legacy). For new projects, use the AgentCore CLI.\n\nNo one did anything wrong. AWS released a replacement, `@aws/agentcore`\n\n, 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.\n\nSome 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.\n\nSo I found a use case. `strands-agents/sdk-python`\n\n, the agent framework’s repository, had been renamed to `strands-agents/harness-sdk`\n\n. *hmmmm! 🤔*.\n\nYou can see the redirect here:\n\nThe response is **301 Moved Permanently**. Follow it, and you land on `harness-sdk`\n\n. Nothing breaks. Old links still resolve because that is what a 301 is for. `pip install strands-agents`\n\nalso 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.\n\nThat is why the rename is easy to miss. Every signal you would normally check looks fine.\n\nI 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.\n\nTools already exist for outdated dependencies: Dependabot, Renovate, `npm outdated`\n\n, and `pip list --outdated`\n\n. They compare the version you pinned with the latest version, then report the difference.\n\nThat works well for version gaps, but it misses three other problems:\n\n| What happened | What the version number shows |\n|---|---|\nThe repo was renamed\n|\nNothing. Your old name still resolves. |\nThe project was archived\n|\nNothing. The last version is still the latest version. |\nThe notes say “deprecated, migrate to X”\n|\nNothing. That’s prose, in a release body. |\n\nNumber 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.\n\nNumber 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.\n\nI called the project **Release Radar**.\n\nYou 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:\n\nThat is the whole product. I kept it small enough to finish in an afternoon, but useful enough to catch a real problem.\n\nTwo parts of the stack are easy to confuse:\n\nJust to let you know, this is a 2-part series;\n\n1️⃣ This activity (this post you are reading) builds the agent and checks its deterministic function logic locally.\n\n2️⃣ The next post moves the complete agent into AgentCore.\n\nFirst, remove the conflicting CLI:\n\n```\npip uninstall bedrock-agentcore-starter-toolkit\n# Or depending on how you installed it: \n# pipx uninstall\n# uv tool uninstall\n\nnpm install -g @aws/agentcore #install agentcore\n\nwhich -a agentcore  # Must print exactly ONE path.\n```\n\nDo not skip `which -a`\n\n. If it prints two paths, the old CLI is shadowing the new one. Every confusing error that follows will trace back to that conflict.\n\nNext, 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.\n\n```\nagentcore create  # Name it: release-radar\ncd release-radar\n```\n\nThe command creates two directories: configuration in `agentcore/`\n\nand your code in `app/`\n\n. You only need to edit `app/`\n\n.\n\nA Strands tool is a regular Python function with a decorator. Its docstring is routing logic, not ordinary documentation.\n\nThe 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.\n\nThe second paragraph below does most of the routing work:\n\n``` python\nimport json\nimport os\nimport urllib.error\nimport urllib.request\n\nfrom strands import tool\n\nAPI = \"https://api.github.com\"\n\ndef _get(path: str) -> dict:\n    \"\"\"GET a GitHub API path. Never raises—errors come back as data.\"\"\"\n    req = urllib.request.Request(\n        API + path,\n        headers={\n            \"Accept\": \"application/vnd.github+json\",\n            \"User-Agent\": \"release-radar\",\n        },\n    )\n\n    # Lifts the anonymous rate limit from 60/hr to 5000/hr.\n    if token := os.environ.get(\"GITHUB_TOKEN\"):\n        req.add_header(\"Authorization\", f\"Bearer {token}\")\n\n    try:\n        with urllib.request.urlopen(req, timeout=10) as r:\n            return json.load(r)\n    except urllib.error.HTTPError as e:\n        return {\"_error\": f\"HTTP {e.code}\"}\n    except Exception as e:\n        return {\"_error\": str(e)[:120]}\n\n@tool\ndef repo_status(owner: str, repo: str) -> dict:\n    \"\"\"Look up a GitHub repository's current identity and health.\n\n    Returns the canonical full_name, which differs from the requested\n    owner/repo when the project has been renamed. Also reports whether\n    the repo is archived and when it was last pushed to. Call this\n    FIRST for every dependency—a rename or archive matters more than\n    any version gap.\n    \"\"\"\n    asked = f\"{owner}/{repo}\"\n    d = _get(f\"/repos/{owner}/{repo}\")\n    if \"_error\" in d:\n        return {\"requested\": asked, \"error\": d[\"_error\"]}\n\n    return {\n        \"requested\": asked,\n        \"canonical\": d[\"full_name\"],\n        \"renamed\": d[\"full_name\"].lower() != asked.lower(),\n        \"archived\": d[\"archived\"],\n        \"description\": (d[\"description\"] or \"\")[:280],\n        \"last_push\": d[\"pushed_at\"],\n        \"stars\": d[\"stargazers_count\"],\n    }\n```\n\n`_get`\n\nnever raises. It returns errors as `{\"_error\": ...}`\n\ndata. 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.\n\nSo the rename check is one line: `d[\"full_name\"].lower() != asked.lower()`\n\n. GitHub’s API follows the redirect and returns the canonical name. Ask for `strands-agents/sdk-python`\n\n, and it returns `strands-agents/harness-sdk`\n\n. Comparing the two reveals the rename. That one line is why this project exists.\n\nThe second tool fetches release notes. Together with the description returned by `repo_status`\n\n, this gives the model prose to inspect for words such as *deprecated*, *legacy*, *superseded*, and *migrate*:\n\n``` php\n@tool\ndef latest_release(owner: str, repo: str) -> dict:\n    \"\"\"Fetch the most recent published release for a repository.\n\n    Returns the tag name, publish date, and the opening of the release\n    notes. Read the notes for deprecation and breaking-change language.\n    \"\"\"\n    d = _get(f\"/repos/{owner}/{repo}/releases/latest\")\n    if \"_error\" in d:\n        return {\n            \"repo\": f\"{owner}/{repo}\",\n            \"error\": d[\"_error\"],\n            \"hint\": \"404 here usually means the repo publishes tags, not releases\",\n        }\n\n    return {\n        \"repo\": f\"{owner}/{repo}\",\n        \"tag\": d[\"tag_name\"],\n        \"published\": d[\"published_at\"],\n        \"notes\": (d.get(\"body\") or \"\")[:600],\n    }\n```\n\nThis field `hint`\n\nis 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.\n\n*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.*\n\nThe third tool compares versions. Most of the code is parsing because version tags come in forms such as `1.2.3`\n\n, `v1.2.3`\n\n, and, in Strands’ case, `python/v1.54.0`\n\n:\n\n``` php\n@tool\ndef version_gap(pinned: str, latest: str) -> dict:\n    \"\"\"Compare a pinned version against the latest release tag.\n\n    Handles common prefixes (v1.2.3, python/v1.2.3) and returns how many\n    major, minor, and patch releases the pin is behind.\n    \"\"\"\n\n    def parts(v: str) -> tuple:\n        tail = v.strip().rsplit(\"/\", 1)[-1].lstrip(\"vV\")\n        core = tail.split(\"-\")[0].split(\"+\")[0]\n        out = []\n        for chunk in core.split(\".\")[:3]:\n            digits = \"\".join(c for c in chunk if c.isdigit())\n            out.append(int(digits) if digits else 0)\n        while len(out) < 3:\n            out.append(0)\n        return tuple(out)\n\n    p, l = parts(pinned), parts(latest)\n    level = (\"major\", \"minor\", \"patch\")\n    behind = None\n    for i in range(3):\n        if l[i] != p[i]:\n            behind = level[i] if l[i] > p[i] else None\n            break\n\n    return {\n        \"pinned\": pinned,\n        \"latest\": latest,\n        \"behind_by\": behind,\n        \"current\": p >= l,\n    }\n```\n\nMy first attempt at the comparison loop was wrong. Because this function contains the project’s only substantial logic, I added asserts:\n\n```\nif __name__ == \"__main__\":\n    assert version_gap(\"1.0.0\", \"1.0.0\")[\"current\"] is True\n    assert version_gap(\"v0.1.0\", \"v0.28.1\")[\"behind_by\"] == \"minor\"\n    assert version_gap(\"1.2.3\", \"2.0.0\")[\"behind_by\"] == \"major\"\n    assert version_gap(\"python/v1.53.0\", \"python/v1.54.0\")[\"behind_by\"] == \"minor\"\n    assert version_gap(\"2.0.0\", \"1.9.9\")[\"behind_by\"] is None\n    print(\"version_gap ok\")\n```\n\nThe last assert failed.\n\nMy first version found the earliest position where the latest version exceeded the pinned version:\n\n```\nbehind = next((level[i] for i in range(3) if l[i] > p[i]), None)\n```\n\nTry that with `2.0.0`\n\nand `1.9.9`\n\n. At the major position, `1 > 2`\n\nis false. At the minor position, `9 > 0`\n\nis true. The function therefore reports “minor behind” even though the dependency is a full major version ahead.\n\nThe 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`\n\nloop does that with a `break`\n\n.\n\nI 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.\n\nFive 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`\n\nis arithmetic, and arithmetic is easy to check.\n\nRun the checks before continuing:\n\n```\nuv run python tools.py  # → version_gap ok\npython\nfrom strands import Agent\n\nfrom tools import latest_release, repo_status, version_gap\n\nSYSTEM = \"\"\"You audit pinned software dependencies.\n\nFor each `owner/repo@version` the user gives you:\n\n 1. Call repo_status first. If `renamed` is true or `archived` is\n    true, that is the headline; report it before anything else.\n 2. Read the repository description and latest release notes for\n    deprecated, legacy, superseded, migrate, or breaking.\n 3. Call version_gap to measure the distance.\n\nThen assign exactly one verdict per dependency:\n\n BLOCKED  renamed, archived, or description/notes say deprecated/legacy\n BEHIND   major or minor releases behind\n OK       current, or patch-behind only\n\nOutput one line per dependency: VERDICT  owner/repo  one-clause reason.\n\nNo preamble. If a tool returns an error, say so and move on.\"\"\"\n\nagent = Agent(\n    system_prompt=SYSTEM,\n    tools=[repo_status, latest_release, version_gap],\n)\n```\n\nThe 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`\n\nis easy to `grep`\n\nand act on later.\n\n`agentcore create`\n\nwill generate a richer `main.py`\n\nthan the version above. It will include session caching, MCP wiring, and a two-argument `invoke(payload, context)`\n\nentry point imported from `bedrock_agentcore.runtime`\n\n. The shorter version is easier to learn from, but in practice you should modify the generated file rather than replace it. Add `tools.py`\n\n, add one import, swap the system prompt, and replace the demo tool. That is three edits.\n\nNow test the three dependencies from the start of this post:\n\n```\nAudit these:\n\naws/bedrock-agentcore-starter-toolkit@0.1.0\nstrands-agents/sdk-python@1.0.0\naws/agentcore-cli@0.28.1\n```\n\nThe last pin was current when I wrote this. Run `npm view @aws/agentcore version`\n\nto get today’s version. If it has moved, you will get a live **BEHIND** result alongside the two **BLOCKED** results.\n\nThis is what the agent returned:\n\nThe 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`\n\nis probably not being called. Check that its docstring survived the copy.\n\nThe 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.\n\nThat 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.\n\nThe 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 🖥️:\n\nI 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.*\n\nThis 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`\n\nno longer returns a 301, something moved again. That is more useful to know than whether this post still looks trustworthy.\n\nHere’s my [GitHub repo](https://github.com/coozgan/release-radar.git) for the code used in this project.", "url": "https://wpnews.pro/news/the-tutorials-moved-faster-than-i-did-building-an-agent-that-catches-deprecated", "canonical_source": "https://dev.to/joshyfruit/the-tutorials-moved-faster-than-i-did-building-an-agent-that-catches-deprecated-dependencies-eh0", "published_at": "2026-09-03 06:08:52+00:00", "updated_at": "2026-09-03 06:22:49.126675+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "large-language-models"], "entities": ["Amazon Bedrock AgentCore", "AWS", "Release Radar", "Strands Agents", "GitHub", "PyPI", "Dependabot", "Renovate"], "alternates": {"html": "https://wpnews.pro/news/the-tutorials-moved-faster-than-i-did-building-an-agent-that-catches-deprecated", "markdown": "https://wpnews.pro/news/the-tutorials-moved-faster-than-i-did-building-an-agent-that-catches-deprecated.md", "text": "https://wpnews.pro/news/the-tutorials-moved-faster-than-i-did-building-an-agent-that-catches-deprecated.txt", "jsonld": "https://wpnews.pro/news/the-tutorials-moved-faster-than-i-did-building-an-agent-that-catches-deprecated.jsonld"}}