AWS Builder Center
I wanted better Strava titles and descriptions. I ended up with a multi-agent app.

I wanted better Strava titles and descriptions. I ended up with a multi-agent app.

From a post-it note to a multi-agent AI coach: my Strava data, Bedrock AgentCore, a memory that learns my style, and every mistake left in the story.

Solutions Architect at AWS France ☁️

How it started: two side projects and a conversation

It did not start with a plan. It started with a conversation with a colleague, about how each of us was making his Strava cooler on personal time. He went for direct LLM calls to Amazon Bedrock . Me, I immediately wanted to play with Bedrock AgentCore, released not so long before. We compared approaches, stole ideas from each other. And I had a dumb, concrete need.
I had taken up running, so I had taken up Strava. And I was tired of writing my descriptions by hand. At the start, the idea fit on a post-it note: what if an AI wrote my titles and descriptions for me, in my style?
Mine took shape, feature after feature, and became something else: Strava AI Boost, a serverless multi-agent app. First a simple description enhancer. Then, brick by brick, an actual coach.
Here is how you go from a post-it note to that, following the real git history, turns left in.

Two minutes of vocabulary, before diving in

I am a Solutions Architect, I cannot help it: the bricks before the building. If you are comfortable, skip this section.
  • An LLM is the raw language model: text in, text out. Alone, it does nothing else.
  • An agent is an LLM put in a loop with tools: actions it can decide to call (search my activities, read my plan). The loop changes everything: the model reasons, acts, reads the result, starts again.
  • Memory is what survives between two conversations: without it, every exchange starts from zero.
  • Guardrails are the safety filter on input and output: inappropriate content, prompt injections, forbidden topics.
  • And Amazon Bedrock AgentCore  is the managed platform hosting all of it on AWS: the Runtime that runs the agents, the Memory, the Guardrails, observability, evaluations. The foundations, so I do not spend my evenings building them myself.
The whole rest of the post stands on these five words.

🧱 The project grew in layers, not all at once

Each initial pain became a layer:
  1. The effort: writing a description after every run, I just never did it. So layer 1: enhance the title and description in my style.
  2. The doubt: does my session match what I was supposed to do? So layer 2: match my training plan (planned vs done).
  3. The perspective: no real read on my progression over time. So layer 3: cross-reference my load and recovery metrics.
  4. Then I wanted to talk to it. So layer 4: a conversational coach.
And cutting across all of it, a loop that learns my writing style without me doing anything.
Before unrolling the story, the map: what the system does today for every activity, from the run to the published text, learning loop included. The whole post tells how these bricks arrived, one by one.
Flow of one run: Strava webhook, a 2-minute window for my feelings and the Enduraw report, data fetched from Strava, Campus Coach and Intervals.icu, facts computed in code, content and coach agents, verification, then publish to Strava. My edits feed AgentCore Memory.

December 2025: the bootstrap (spec first, pipeline second)

The very first commits are not code. They are three files: req v1, design v1, tasks v1. This is Kiro's spec-driven development : before writing a line, the agent and I set the requirements (testable criteria), the design, and the tasks. The code comes after, each task validated against the spec. On a solo side project, that is what keeps you from going in every direction on a motivated evening.
The starting point was Strava's developer program , well documented: OAuth for authorization, a clean REST API to read activities and laps, and webhooks that ping you on every new activity. Nothing to force, everything is made to build on top of.
In two weeks the skeleton was there, already a real serverless pipeline, not a script:
1
Strava webhook -> SQS -> Step Functions -> fetch + generation -> assembly -> Strava update
The choices that carried everything:
  • SQS + DLQ: I answer the webhook in under 5 seconds (HMAC signature validated), process behind it, and if something breaks the message lands in a dead-letter queue instead of vanishing. I can replay it.
  • Step Functions: a readable workflow, and above all parallelizable later.
  • Anti infinite loop: when I write the description back to Strava, Strava sends me an "update" webhook... which would trigger another run. Mandatory guards (skip already-processed activities, cooldown on failures). The kind of detail you only meet in production.
At that point, a single content agent, no coach yet. But the foundation was set.

Four data sources that do not talk to each other

Very quickly the real problem showed up: my data was scattered across four services ignoring each other.
  • Strava , the social network of sport: paces, heart rate, kilometer splits, personal records, the kudos from friends.
  • Campus Coach , a coaching app very popular in France among serious runners (think Runna): my training plan built by real coaches, with blocks and target paces.
  • Intervals.icu , the analysis tool of the data nerds, built solo by David Tinker , a developer and cyclist from Cape Town: started in 2018 as a side project to test a framework, became the reference of the endurance community. A trajectory that speaks to me. The full chain: I run with an Apple Watch: not the best choice for running, I know, but I love the Apple ecosystem too much lol. Everything it records lands in Apple Health on my iPhone, and Apple exposes no cloud API to read that data from a server. HealthFit  is what unlocks everything (another solo craftsman's work, by Stephane Lizeray ): its native integration with Intervals.icu pushes all my data up automatically, sleep, resting HR and HRV included. Intervals.icu digests it, computes metrics on top, and serves it all through a clean API. The tool does much more than my use of it; I take what my coach needs: training load (CTL/ATL/form), HRV, sleep, VO2max, resting HR, cardiac drift. Nothing more.
  • Enduraw , a third-party micro-analysis app: it republishes to Strava the wind-adjusted pace, the day's weather and the elevation cost.
And let's be honest: I did my cherry picking, each tool gives me the piece I need, and that works just fine.
Strava shows the session but not my sleep. Campus Coach has the plan but does not know whether I followed it. Intervals.icu does the analysis but not the writing. The whole project, at its core, is bringing these four silos together to produce content and coaching. The logical view below shows that assembly.
Architecture: Strava webhook to Lambda, SQS and Step Functions, two agents on AgentCore Runtime running in parallel, shared AgentCore Memory, DynamoDB, a coach chat runtime, and a React frontend on CloudFront with Cognito.

🧩 The assembly for real: my long run of August 15

Rather than a theoretical diagram, a real recent activity : long run, heat wave. My raw description, typed from the phone in my two minutes (in French, rough translation below):
Sortie « longue » de la semaine en intégrant bloc actif. Impossible de me lever tôt pour courir à la fraîche. Du coup, run sous le cagnard. En me mouillant à toutes les fontaines sur le chemin.
("Long run of the week with an active block. Could not get up early to run in the cool. So, run under the blazing sun. Soaking myself at every fountain on the way.")
During the same window, Enduraw appends its report right after: heat 25°C = 0'09"/km lost, wind = 0'06"/km, sweat rate 0.9L/h.
Under the hood, six steps: the webhook answers in under 5 seconds, the message waits 2 minutes in the queue, everything gets fetched, everything deterministic is computed in code, the two agents run in parallel, and the verifier checks each output against the computed facts before publishing. The full dissection, step by step with the real data of this very activity, lives in the repo as a worked example .
What each source brings to the agent's context:
  • 🟠 Strava (laps): 11 raw laps, speed AND average heart rate each. The code reads a 3.9km warmup, 4 fractions of 5min at 5:23-5:26/km (HR 158-162 bpm), 2min recoveries, a cooldown. Paces computed in code, never by the model.
  • 🟣 Campus Coach: the session planned that day, "Long Run with active block 4x5min Tempo", target 5:26/km. The deterministic matching binds it to the activity: the agent knows this was THE session of the plan, not an improvised run.
  • 🔴 Intervals.icu: VO2max at 53.45 (+1.1 vs 30-day average), resting HR 51, form -3.3. The trend, not just the day.
  • 🔵 Enduraw: how much the day's conditions cost me, in numbers (heat 0'09"/km, wind 0'06"/km). A free integration: connect your Strava once, the report pastes itself into every description during the two-minute window.
  • 🟡 My two minutes: the blazing sun, the fountains. The only ingredient nobody else can provide.
  • ⚫ The profile and the memory: the Boulogne half goal in November, my tone.
And the agent's output, published as-is on Strava. Excerpts (translated), with the source of each sentence:
Could not get up early to run in the cool. So, run under the blazing sun at 25°C. Survival strategy: every fountain of the route became my best ally. *(🟡 my words, kept)*
📋 Campus Coach session: Long Run with active block 4x5min Tempo + 2min recovery ``(🟣 the plan)`` • Tempo 1-4: 920-926m in 5:00 at 5:23-5:26/km, HR 158-162bpm (5:26/km target validated!) *(🟠 the laps, figures computed in code)*
My VO2max climbs to 53.45ml/kg/min (+1.1 vs 30-day average), the progression is there. *(🔴 Intervals.icu)*
🌡️ Enduraw analyzes the conditions: heat cost me 0'09"/km, wind 0'06"/km [...] *(🔵 Enduraw)*
This capacity to hold the pace despite the heat is an asset for the Boulogne half in November. *(⚫ the profile)*
Fun fact: at 25°C, my sweat rate reaches 0.9L/h according to Enduraw. Good thing I emptied all those fountains, otherwise I was finishing as a dried grape 💧 ``(🟡 + 🔵, blended)
Four silos plus my feelings, melted into one text that sounds like me.
A word about Enduraw, because the tool deserves its shout-out. Enduraw, contraction of endurance and raw, is the performance laboratory of Joseph Mestrallet , an engineer  based in Chamonix: he built the race strategies of Tom Evans and Ruth Croft, winners of the UTMB 2025, and his lab also serves amateurs like me. I am exactly his target, the tech guy with a passion for sport, playing more than optimizing. And he has a line that resonates hard with this project: "Data can tell you anything if you do not know how to tell what is relevant." He says it about his sensors and lactates. Me, I spent eight months learning it with LLMs.

The first five months: "enhancer" mode

For five months the project did one thing, better and better: enriching my titles and descriptions in my style. Four structural pieces appeared:
The secret sauce: my two minutes. You saw it live in the assembly above: the pipeline waits two minutes, the window where I dump my raw sensations into the description from the phone. The feelings of the day are the one ingredient the agent cannot invent, and the same delay lets the Enduraw report land. One delay, two sources of context.
The passive learning loop. My favorite idea. When I hand-edit a generated description, a nightly Lambda compares the AI version and my final version (a simple text similarity). Below a strict threshold, it means I really edited: the diff feeds the memory, a preference strategy extracts my tastes from it (length, tone, emojis, structure), and the agent reads those preferences on the next run. Zero friction: I correct the text exactly as I would have anyway, and the system learns my style on its own. And it gets better over time, edit after edit. That is what I find really strong in the agentic part: not the generated text, the loop that learns.
The whole trick fits in one call. The diff is written to AgentCore Memory as a plain two-turn conversation, and the managed preference strategy does all the extraction work:
1
2
3
4
5
6
7
8
9
10
11
# Nightly Lambda: below 99.5% similarity, the edit becomes a memory event
client.create_event(
memoryId=MEMORY_ID, actorId=user_id,
sessionId=f"feedback-{activity_id}",
payload=[
{"conversational": {"role": "ASSISTANT",
"content": {"text": f"Generated description:\n{enhanced}"}}},
{"conversational": {"role": "USER",
"content": {"text": f"I edited the description to:\n{final}"}}},
],
)
No embedding pipeline, no vector store to run, no consolidation job to write. I frame the edit as "here is what you said, here is what I changed it to", and the strategy consolidates preferences over time.
From invisible processing to application. At the beginning, no app at all: a background job updating my Strava, no screen, no button. It became an application the day I wanted two things: an AgentCore demo asset, and my own overview (progression, quality metrics, coach feedback, debriefs). A dashboard was born, then pages. On the design side, I stayed deliberately basic: a clean interface that does the job, nothing more, all my energy went to the functional. If I wanted to push the experience one day, tools like Open Design would let me do it without spending my evenings on it. It is on the next steps list.
The hardening. First, Bedrock Guardrails on input, the bouncer of the system: it blocks prompt injections and inappropriate content before the model sees it. Heavy for a single-user app? My agents read text coming from outside, and a Strava description is an injection vector like any other. Then the anti-AI writing rules (banned cliches, banned dashes), the frontend migrated from Flask to React, Intervals.icu wired in, the test bar rising. And a cost pass in April, which I come back to below.

🛠️ The first real turn: Campus Coach, from Browser Tool to API

My training plan lives on Campus Coach. In January, no public API, and their app showed nothing usable in the browser developer tools. So I pulled out the most impressive AgentCore building block to demo: the Browser Tool. An agent that logs into the site, navigates, reads the DOM, extracts the plan. It worked, and it was satisfying to show.
It was also fragile. Sensitive to any page change, to cookie popups, to timeouts. Frequent cold starts (a headless browser is expensive). The scraping broke regularly.
Then Campus Coach shipped a new version of their app. The same evening, reflex: F12, Network tab, and I clicked everywhere in their interface watching what went over the wire. Bingo: the endpoints their own frontend was using. One login, one GET, everything was there. I migrated in a day: the data arrived deterministically, reliably, fast. No more cookie popup breaking everything, no more headless browser timeout. A few weeks later I completely decommissioned the Browser Tool agent: runtime deleted, code and scripts cleaned up. Deleting something you spent hours making work, it stings. And it feels great.
A slightly uncomfortable lesson for a project you like to demo: the most impressive building block is not the right building block. The Browser Tool is an excellent fallback when there is no API, not a long-term solution. As soon as an API exists, you migrate, and you dare to delete. Nothing beats a deterministic API. Nothing.

🏃 May 10, 2026: the coach is born in a single day

The real turning point fits in one day of git history: May 10, seven commits in a row, Phase 1 through Phase 7.
By then, the ambition had outgrown the descriptions. All this data finally crossing in one place, and nobody to read it. What I wanted was a coach persona: someone who knows my history, my plan, my load, my sleep, and gives me real advice. Not a session summary, an opinion.
The trigger: enriching a description and coaching an athlete are not the same job.
  • The enhancer must be expressive, stick to my style, slip one or two signals into a public text.
  • The coach must be analytical, look at long history, talk trends and progression, not replay what happened during the session.
One prompt doing both does both badly. So I created a second agent, a dedicated coach with its own personality and its own prompt, launched in parallel with the enhancer by Step Functions. A detail that matters: the coach has a catch. If it fails, the activity still gets enhanced, it never breaks the flow. A coach missing one piece of advice is fine. A description that never ships is not. The next day, May 11, that same agent gained a second mode: conversation. I could finally ask it questions.

⚡ May 2026: the giant month

Once the coach was born, everything went fast: May alone concentrates about 160 of the project's commits.
  • 💬 The conversational coach: ask in plain language ("do I run tomorrow or do I recover?"), get an answer grounded on my real data.
  • 📊 The Coach page: each session's feedback, volume and pace trends, the athlete profile and the week's plan, all in one place.
  • 🎧 The per-activity audio debrief: after a run, I walk five minutes with zero desire to read. So: a short LLM script, turned into one minute of voice by Amazon Polly  (the AWS text-to-speech service, surprisingly natural). The debrief is waiting for me before I catch my breath.
  • 🎙️ The weekly audio recap: every Sunday evening, a small podcast that closes the week and frames the next one.
  • 🔋 The recovery status widget: form, VO2max, resting HR and sleep from Intervals.icu, with a tip of the day ("well rested, good moment for an intense session").
  • 🏋️ Strength program tracking: loads extracted from what I naturally write ("bench 80x10"), progression history building itself.
  • 🔌 The Campus Coach migration to the REST API on May 21: the end of scraping.
Three screens of the app, for real: activity detail, the Coach page with recovery status and audio recaps, and the trends.
Three app screens: an activity with its AI-generated description, the coach dashboard with weekly volume and recovery metrics, and the weekly trends chart.
The evening side project became a real app. 🚀
It is also when the project fully played its lab role, and it became a ritual. An AgentCore building block ships, I ask myself one question: which piece of MY problem does it solve? The Runtime hosted my agents. The Memory gave them continuity. The Browser Tool filled the no-API gap. Later, managed Evaluations kept my prompts honest, and the episodic memory strategy gave the coach consolidated memories. When a brick solved nothing for me, I did not keep it.
Same ritual with the agentic protocols: MCP, A2A, then AG-UI , the protocol that standardizes the dialogue between an interface and an agent (what MCP does for tools, AG-UI does for the frontend). Rather than reading the spec, I plugged it into my chat.
Same logic on the tooling side: every new Claude model, every Kiro novelty, tested here first, on a project with real stakes. A regression shows up the same evening, on my own Strava. I do not read the release notes, I run them.

Personalization: three layers and a memory with two roles

Without personalization, content and coaching sound generic: "Nice run, keep it up". Three layers against that, each killing one flavor of generic.
  • Static: who I am. My age, my marathon goal, my pace zones, my records, my strength program. It is the difference between "run at a comfortable pace" and "run between 5:50 and 6:30, that is YOUR endurance zone". Small detail I like: my max heart rate is not frozen, the pipeline updates it as soon as an activity shows a higher value.
  • Dynamic: how I am doing, right now. Four weeks of history, CTL/ATL load and form from Intervals.icu, recovery, plan adherence. The same session does not call for the same advice when my form rises or when I am digging myself a hole.
  • Learned: what no form can capture. The AgentCore Memory, fed by the nightly loop and by every coach feedback.
And here is the technical bit I find elegant: one single memory resource, two roles separated by namespaces, not two memories.
  • Content role: it learns my editorial style (length, tone, emojis, structure), extracted from my edits and consolidated over time.
  • Coach role: it accumulates its observations about me (load progression in strength training, my cardiac drift on long runs, the advice already given).
One resource to operate, one IAM role, one line on the bill. A new agent plugs its namespace and inherits everything.

🔍 July 2026: the memory I thought was alive was dead

First, a confession about the pace, because it explains everything that follows. This project is an evening thing, in experimental mode: one feature here, one fix there, and we will see tomorrow. No disciplined monitoring, no regular review. Exactly the ground where a silent bug thrives.
This summer, while preparing the open source release, I audited the memory for real. And I found much worse than what I was looking for: the loop had been broken from the start.
  • The coach was reading a memory namespace that no strategy was feeding. It thought it remembered. It remembered nothing.
  • The weekly audio recap was broken three ways: wrong namespace, an old API shape, and an IAM role with no bedrock-agentcore permission at all. Silently.
The word that matters is silently. Nothing crashed. The logs were green. The feature "existed". It did nothing. For months, I read the coach's feedback thinking it was built on its memory. In plain words, the coach was looking for its memories in one drawer, and everything was being stored in another.
The fixes forced me to understand AgentCore Memory for real: resolving the strategy identifier at runtime instead of hardcoding it, a search query adapted to the session type, and unifying every namespace on a single pattern.
The repaired read is small, and every line exists because of a bug:
1
2
3
4
5
6
7
8
9
10
11
12
13
# The coach reading its long-term observations (post-fix)
response = client.retrieve_memory_records(
memoryId=memory_id,
namespace="/strategies/", # prefix: new strategies picked up free
searchCriteria={
"searchQuery": _session_type_query(activity), # strength != intervals
"topK": 8,
},
)
records = [
r["content"]["text"] for r in response["memoryRecordSummaries"]
if any(f"/actors/{user_id}/" in ns for ns in r.get("namespaces", []))
]
The prefix search means a new strategy starts feeding the coach without a code change. The actor filter is the user isolation. And the query changes with the session type, because "load progression on bench press" and "interval pacing drift" do not live in the same records. The broken version read a namespace nothing wrote to, and none of this surfaced in any log. In the end, one memory shared by the three agents, with three strategies, each a managed extractor with its own specialty:
  • the semantic one extracts durable facts ("he is preparing a marathon", "his cardiac drift is dropping on long runs");
  • the user preferences one distills my writing tastes from my edits (length, tone, emojis, structure): it is the one carrying the learning loop;
  • the episodic one keeps one memory per session and periodically writes a consolidated reflection, the step back a human coach takes rereading his notebook.
That last one, I finally watched it generate an episode and a reflection within fifteen minutes. The moment the loop actually closed.
Lesson: an AI feature that does not crash is not a feature that works. The only way to know is to open the database: check what it really writes, and what it gets back when it reads.

💬 The conversational coach, and the proxy that never needed to exist

Layer 4 was the one I wanted from the start: talking to it. Asking my question like I would ask a human coach and getting an answer built on MY data. To get there, I corrected two false good ideas:
False idea 1: dump everything into the prompt. The first version of the chat pushed one big frozen context block and hoped the model would find its way. That does not scale to open questions. The right answer: make the agent agentic, give it tools (query activities, the plan, pace zones, metrics, its own past observations) and let it fetch exactly the data each question needs. The tool loop runs server-side. Along the way, the chat moved to a third dedicated runtime, the shared memory keeping the two coaches consistent.
False idea 2: you need a Lambda proxy to stream. I was convinced I needed a Lambda between the browser and the agent to relay the stream, it was already on my list. Then I checked: the AgentCore data plane already returns the right CORS headers. The browser posts directly to the agent, no proxy. I crossed it off my list before writing its first line.
The chat speaks the AG-UI protocol over Server-Sent Events, the browser posts straight to the AgentCore data plane, authentication goes through the Cognito token (the athlete's identity is derived server-side from the JWT, never from the request). One less Lambda, and the chat answers live instead of making you wait for a block of text.
Lesson: before adding the infra piece you add by reflex, check what the platform already does. I almost coded a Lambda for nothing.

Evals, or the day I stopped taking my agents at their word

When you change prompts often, you start to be afraid: did my last change break quality? And when you reread the outputs, one thing becomes obvious: the model hallucinates. Not often. But always with confidence.
My favorite hallucinations, all lived in production on MY sessions:
  • My coach proudly announces intervals at "3:16/km". I wish. Tracing it back: the model had taken a raw speed from a lap (3.26 m/s) and read it as decimal minutes. An invented conversion, delivered with confidence.
  • A two-block strength session, done in full. The published description: "Block 1/2". The model had read "Part 1 of the day" in MY own description... except I meant my first session of the day, not a block.
  • And the LLM judge of my evals flagging an "invented number" on an interval... when the number was correct. The judge was miscounting. Hallucination squared: the controller hallucinating about the controlled.
The answer I built has three floors, from the most deterministic to the most probabilistic:
  1. Everything that can be computed is computed in code, never by the model: paces from lap speeds, block and interval counting, plan matching. The prompt declares these numbers as the only allowed source. An LLM should never do arithmetic a for loop can do.
  2. A verifier rereads every output before publication and compares it to the computed numbers. Contradiction found? Regenerate once, handing the model the precise diagnosis. Still wrong? The faulty sentence is removed. An amputated description beats an invented number on my Strava.
  3. The regression harness: deterministic checks (banned cliches, dashes, length, emoji policy) for a few cents per run, and managed "LLM-as-a-judge" evaluators. From its very first runs, it caught two real agent crashes and four self-contradicting examples in my own prompts: my "positive" examples contained the very cliches I was banning.
And the rule carved by the miscounting judge: deterministic checks are the gates (they block), LLM judges are signals (they warn). You never block a deployment on the opinion of a probabilistic judge.
The same principle runs everywhere in the project: deterministic before the LLM. Workout classification, plan matching (with a confidence threshold below which nothing gets linked), the eval gates. Only the best candidate reaches the prompt. The LLM does what it does well, the rest is testable code.

Cost: the part where I sweated

A 24/7 system with three agents, voice generation and a frontend: about 5 dollars a month. First month under 2 dollars, a 7.44 spike in April that triggered my optimization pass, then stabilization around 5 to 6. Roughly 97% of the bill is AgentCore, everything else is negligible.
The optimization levers: measure before anything (cost tags, per-agent attribution), the right model for each task (Haiku for short tasks, Sonnet for rich generation), prompt caching on reused context (a Bedrock cachePoint after the static part of the system prompt), and removing what was not needed (a custom monitoring stack dropped for the native GenAI dashboard).
The caching one is worth showing, because the whole point is where you cut the prompt:
1
2
3
4
5
6
7
# System prompt split: static rules cached, per-user context after the cut
if len(static_system_prompt) >= 4096: # Bedrock min cache ~1024 tokens
system_prompt = [
SystemContentBlock(text=static_system_prompt),
SystemContentBlock(cachePoint={"type": "default"}),
SystemContentBlock(text=dynamic_system_prompt), # profile, prefs
]
Everything that never changes (writing rules, banned cliches, output format) sits before the cache point and is billed at the cached rate on every cache hit (the cache has a short TTL, so it pays off on bursts and retries, not on isolated runs). Everything per-user or per-activity comes after. Put one dynamic byte before the cut and the cache never hits again.
Lesson: on serverless plus GenAI, cost is a feature. It gets designed, measured and refactored like everything else.
The detailed view below shows everything living around the happy path.
Detailed architecture: API Gateway with Cognito, Secrets Manager, three AgentCore runtimes with shared Memory, Evaluations and Observability, the event pipeline, Polly voice features, EventBridge schedulers and the nightly learning-loop Lambda.

Did I really need AgentCore for a personal project? No. And that is the point.

Let me say it plainly, it is the first question a senior reader asks: for a single-user app, a Lambda, a Bedrock InvokeModel and a DynamoDB table as "memory" would have done it. Cheaper, fewer parts.
I picked AgentCore because learning it was the goal. Which leaves the real question: what did it bring me, concretely?
  • Managed Memory: three strategies (semantic, preferences, episodic) I would not have rewritten cleanly by hand; the preference strategy carries the learning loop.
  • Runtime + AG-UI streaming + customJWT: the token-by-token chat, no proxy, with auth wired to my Cognito pool. That would have been a lot more plumbing done by hand.
  • Managed Evaluations and Observability: the GenAI dashboard and managed judges, ready to use, on a project where I did not want to build my own quality tooling.
And what it did not bring me, I removed: the Browser Tool, spectacular but fragile, decommissioned as soon as an API existed. That is the honest thread of the project: keep the brick that solves a real problem, drop the one that only demos well.
The real dividing line, I would put it like this. As long as your need fits in "one LLM call with some context", a Lambda and InvokeModel are enough, by far. Beyond that, you can always rebuild everything yourself, nothing impossible. But leaning on managed services instead of carrying it all is precisely one of the values of AWS, and AgentCore is the toolbox for agents: build, host, secure, operate. I had both moments in this project: December was clearly before the line, May clearly after. For a personal side project optimized to the dollar, no, I would not recommend AgentCore by default. Here, ramping up on the service was the deliverable, and at 5 dollars a month, it is the cheapest training of my career.

Does it actually work?

The honest question, the one that scares you after eight months. Only one way to answer it is solid: I opened my database and I counted.
The direct proof, the one measuring the core feature: do I publish the generated descriptions without touching them? Pulled from my own database on August 19: out of 192 processed activities, 190 went through the nightly loop, and I edited 61. About two descriptions out of three ship untouched (~68%). And when I do correct, it is mostly a light touch: on my edits, the median similarity between the generated text and my final version sits around 0.93, and only a small share (~10%) are real rewrites.
When I wrote the first version of this post in July, the edit rate had stayed stuck around 35-40% for seven months, and I had resisted writing "it learned, so I edit less": it was false back then. Since then, it moved: 17% in June, 19% in July, 12% so far in August. The loop finally learning? A summer where I edit less? A different mix? I cannot cleanly tell, and I prefer to say it. When I touch the text, it is usually to add a personal note I forgot in my two minutes, not because it is bad. That is the only metric that says "the system does the job I built it for". The rest is side effects.
The side effects, precisely, on my real Strava data since the project started:
  • Twice as many kudos per activity on average (from ~3 to ~6, peaking at 25 on my second marathon), measured through the Strava API, not a feeling. A vanity metric, I assume it: more kudos because I post better, but also because I run more.
  • 11 minutes off my marathon: 4:02 in October, 3:51 in April. A half-marathon PR along the way.
  • 388 runs on the Strava counter; the app enhanced 192 of them since late 2025, not my whole history.
A concrete coach example: recently, a small pain in my right hamstring. The coach, with my load and form in context, told me to take it easy, and I slowed down instead of forcing out of pride. Nothing magical, but having it at the right moment, connected to my real data, that is the value.
And let me be clear about the role I give it: my AI coach is not a planner. For the plan, I trust Campus Coach, it is their job. The app was built by Nicolas Spiess  and Tristan Pawlak , aka Running Addict and Ironuman on YouTube, passionate runners who have been teaching running training for years. Their bet is precisely the human: plans supervised by certified coaches who adjust on runner feedback, not a pure algorithm. One more reason to trust them with the planning. My coach is there to challenge, to reassure, to analyze my fatigue, and to adapt my strength sessions, which the Campus Coach program does not cover. The plan comes from the humans, the challenge comes from the AI.
Correlation, not causation, and I say it clearly. The AI did not make me run faster. But the project got me back to running 🏃: I go out to test my features, I am more consistent in training. Maybe the best side effect a side project can have. 💙

The wink: Strava did the same thing, for 150 million athletes

While I was building my little thing in the evenings, Strava launched Athlete Intelligence: the same problem, for 150 million athletes. Their technical choices, published in an AWS case study: Amazon Bedrock with Claude Haiku and Bedrock Guardrails to keep a positive tone and filter content. Mine too, at my ridiculously small scale.
What makes me smile is the shared lesson: managed services saved them from running infrastructure, so that time went into product value. Same at my place. From the Sunday side project to the 150-million-user product, the same foundations. The only real difference is the need. Athlete Intelligence stays on the surface for me: it sees neither my Campus Coach plan, nor my sleep, nor my Intervals.icu load. That is precisely the hole my app fills. Strava summarizes one session for everyone. Me, I wanted to cross-reference my whole history and my goals, for one athlete. Me.
The wink has a second layer. While they were launching their AI, they also closed the free access to their API: since 2026, you need a paid subscription to keep a developer access. The reason, given by Strava itself: the flood of AI-generated apps and scraping was degrading platform performance, their own figure being developer applications up 448% year-to-date. And the funny part is that I am on both sides of that story: my app is exactly that, an app built with AI hitting their API. With a paid subscription, respected rate limits and a single athlete served, but still.

What I take away from the journey

If the December me, the post-it one, read this, he would not believe me. As of today, in numbers: more than 500 commits over eight months (about 160 in May alone), more than 32,000 lines of code, 8 CDK stacks, 18 Lambdas, 3 AgentCore runtimes, one shared memory with three strategies, more than 700 tests, for about 5 dollars a month. Published open source under the MIT-0 license. Two models only: Claude Sonnet 4.5 for rich generation (content, coach), Claude Haiku 4.5 for short tasks (voice, extraction, memory). An inspiration sample, not a product: single-user by design, manual deployment, dependencies that can break. Owned.
What I keep, in five lines:
  • The LLM is 10% of the problem. Data, reliability and cost are the other 90%.
  • An AI feature that does not crash can be doing nothing at all. Go read what it actually writes.
  • Check what the platform already does before adding your own plumbing.
  • Know how to delete the impressive brick when a simple one is enough.
  • Deterministic where it can be, LLM for the rest. And cost is a feature.
All of it built on personal time, with Kiro. And maybe that is the real point of the project, more than the coach itself: a living lab where every new AgentCore building block, model and tooling release gets tested on a real problem. Staying current by building, not by reading. What I love since AI arrived, and code assistants in particular: there are fewer and fewer brakes to build the use cases that speak to us. We gained one more layer of abstraction. We bring the vision, we stay accountable for the technique, and you still need the foundations not to get walked around. But with today's tools, we build incredible things. What mattered with Kiro is not "the AI writes the code for me", it is the frame: a spec before coding, a project memory, rules that keep me from repeating mistakes. What really changed in eight months is the distance between the idea crossing your mind and a project that runs: it collapsed. And the README still tells the truth, because a test checks it against the code.
If you keep one thing: an AI feature that does not crash is not a feature that works. So be careful lol.
👀 The repo is public if you want to steal patterns: event-driven pipeline, AgentCore Memory, Guardrails, AG-UI streaming. Not a turnkey product: single-user by design, manual deployment, and Strava closed its API to accounts without a paid subscription in 2026 (a fork without one will see 403s). I am not saying it is the best way to do it, but it is the way I did it. The sharing is aspirational, not a solution to redeploy as-is. There is not necessarily one good method anymore: there are many, everyone has to find his own and walk his own learning path. And I will take feedback, issues, contributions, if you are up for it. A story about building, with the failures left in.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article