Inspiration
I grew up watching the hardest part of the week in a house where both parents work, and it was never the work. It was the logistics: who collects whom, from where, and will they get there in time.
So when I looked at the assistants on the market, I noticed they all get this wrong in the same way. They treat a "conflict" as two events at the same time. But look at this Thursday:
- Dad's performance review ends at 4:45 PM, at his office
- The office is 40 minutes from the football ground
- His son needs collecting at 5:00 PM
Nothing overlaps. Every calendar app in the world calls that a clean week. And it cannot be done. No app will tell you that on Monday — you find out at 5:15 on Thursday, when a kid is standing alone outside a football ground.
That gap is the entire project. A calendar shows you your week. It has no idea whether your week is possible.
What it does
LifePilot is a family coordination agent for working parents, built as an MCP server with a voice-only interface. It doesn't list what's happening. It works out whether the week can actually be done by the people available, finds what breaks, decides who should handle it and why, and repairs the plan with the fewest changes.
A parent always approves before anything changes.
- Can-it-actually-happen conflicts. Counts travel time, who can drive, and the family's own rules. "Dad is down for it but arrives 5:25 PM, needs 5 PM. Mom could cover it instead."
- Who should do it, with reasons. "Mom is the only one who can get there."
- Why not X. "Not Grandpa, because your rule says only when no parent is free. Mom can." Every answer points at a specific rule, event or travel time.
- Fix the week. Repairs a broken week with the fewest possible changes and explains every change, then waits for a parent to approve.
- Partner swaps. Checked for feasibility both ways, then held for the other parent, who accepts in her own session.
- School email to plan. Reads school email only, turns it into dated tasks, checks whether they break the week, and proposes a fix.
- Family rules as real constraints. Said once in plain speech, parsed, read back, and enforced by the solver on every plan — so the model can't forget one.
- Role-aware privacy. A child sees a parent's meeting as "Busy", never a title or a place.
How I built it
Two Python processes, one thin browser client, three external services.
The single most important decision: the model is never allowed to decide anything.
- Amazon Bedrock (Nova Micro) understands the sentence and words the answer. That's all.
- The MCP server holds the data, checks reachability, ranks who should cover a lift, and runs the solver.
engine/andsolver/decide; they return a plain-English reason for every change.
This isn't a stylistic preference. Early on I handed Nova the raw JSON from a tool call and asked for a summary. It told me Dad had a piano class (that's his daughter's), invented a date range, and described a child's schedule as "redacted". Every sentence was fluent, and three of four contained a falsehood. So the server now computes the facts in plain English and the model only words them, under a prompt that forbids adding a name, time, place or activity.
The stack
| Layer | Technology |
|---|---|
| Model | Amazon Bedrock, Nova Micro only, plain converse |
| Protocol | Official MCP Python SDK, Streamable HTTP, spec 2025-11-25 negotiated on the wire |
| Cards | MCP Apps (mcp.server.apps) — the proposal card with Approve / Not now |
| Skills | An Agent Skill package (SEP-2640) served by the MCP server |
| Agent loop | Strands Agents SDK |
| Solver | Google OR-Tools CP-SAT, minimal-change repair |
Gmail API, gmail.readonly only, behind a sender allowlist |
|
| Voice | The browser's own Web Speech API |
20 MCP tools are registered; the model routes among 16. The other four — approving, rejecting, listing proposals — are reached by code, because a yes or no is settled before the model is asked.
Security is structural, not a prompt plea
- Email extraction is one call carrying exactly one output-only tool, forced with
toolChoice. No action tool is in scope, so an instruction buried inside an email has nothing to reach for. - Gmail OAuth is served by the MCP server, not the process running the agent, so a token never reaches the model.
- Bodies are discarded after extraction —
school_messagehas no column for them.
Challenges I ran into
I kept a friction log while building — it's 42 entries long and it's in the repo.
The Agent Skill was broken for every Windows judge, and perfect on my machine
The skill catalogue digested path.read_bytes() while serving path.read_text(), whose newline translation turns CRLF into LF. My tree is LF, so the two agreed by luck. core.autocrlf is Git's default on Windows, so every Windows clone got a digest claiming 3413 bytes for the 3344 actually served — and the host rejects a mismatch silently. I only found it by building a tree from git checkout-index and following my own README like a stranger.
That same run found two more things invisible from my working tree
- A
.gitignoreline matched at any depth, so a document I'd promised judges was never in the repo at all. make testfailed from a clean clone while passing locally.
698 tests were green on a machine where three things were broken for everybody else.
Antivirus
Avast intercepts HTTPS, so every AWS call failed with CERTIFICATE_VERIFY_FAILED while Python's own ssl succeeded — the cause was anything but obvious. Worse, Avast corrupts any loopback response whose request carried an MCP-Protocol-Version header, which took a full day to prove was not an SDK bug.
"cost" is a substring of "costume"
Asked when a school costume was due, LifePilot answered "LifePilot keeps no record of money" — about a task it had extracted itself. A cheap keyword guard collided with the family's own vocabulary. That was the sixth time that lesson appeared in the log.
The token limit ate the deadline
Deadlines were appended after the day's events, and the sentence was cut mid-word before reaching them. The Converse response gives no marker on a truncated message — only stopReason, one level up, which is easy to never read.
Accomplishments that I'm proud of
Every claim has a number behind it from make eval — one command, six CSVs, five charts, and zero model calls. Nothing in the README is typed by hand.
| Experiment | Result |
|---|---|
| Conflict detection | A calendar-overlap check scores F1 0.400. LifePilot scores 1.000. Overlap catches 5/5 plain overlaps and 0/10 unreachable pickups — that gap is the whole product. |
| Plan repair | 20 broken weeks: 0% rule-breaking, 0% failed to fix, 0.75 changes per repair, median solve 15 ms (p95 16 ms), and it never moved an immovable event. |
| Stored rules | With rules: 0 violations, 100% acceptance. Without: 100% violations, and it offered a barred carer in every single case. |
| Tool use | 16 tools: 100% tool, 100% argument, 100% answer correctness over 55 real utterances. A merged 9-tool set scores 92.7 / 94.5 / 90.9 — coarser tools are worse, which I report rather than hide. |
| What am I forgetting | Catches 5/5 at-risk tasks with 0 false alarms and 3 days' warning. Deadline-only reminders catch 3/5, raise 2 false alarms, give 1 day, and never see the 3 tasks that only exist in email. |
| Load balance | Pickup spread 0.43 with the soft rule, 5.00 without. |
705 tests, ruff clean, and the whole project cost about $1.50 to build.
That last number matters to me as a student: one voice turn costs $0.000335, or $0.000152 cached. A hard ceiling of three model calls per turn is enforced in the one module that may call Bedrock, so a runaway loop cannot exist. What-if, why-not, reachability and repair are solver work and spend nothing at all.
What I learned
- Built is not the same as reachable. Four separate times I found a feature that was written, tested and completely unreachable by voice. What-if had passing tests and three different phrasings that all failed. A delete path existed and worked, and nothing could call it.
- A test suite that asserts tool results says nothing about what a person hears. Five defects in one day had correct rows in the database and lost them on the way out — a card that never drew tasks, an answer that said "it breaks 2 things" and then named one. That's the inverse of what unit tests catch.
- The published repository is not your working tree, and a fresh-clone run is a test that belongs in the schedule, not in the last week.
- A small model is excellent at understanding and terrible at knowing. Give it the words and take away the decisions, and it stops being a liability.
What's next for LifePilot
find_time— "Which day can I get lunch with Mom at 1?" needs overlapping two people's free gaps. The hard part (free_gaps()) already exists; the tool doesn't, so today it gives a near-miss answer instead of admitting the gap. Admitting it is the honest behaviour.- Session reconnection, so restarting the server doesn't strand a live session.
- Google verification and a CASA assessment, required for restricted Gmail scopes at real scale.
- Real calendar sync and live travel times, replacing seeded data and the fixed travel table.
- A proper user study. I cut it rather than run something thin — a made-up number would be the one figure here that didn't trace to a CSV.
Built With
- agent-skills
- alembic
- alexa
- amazon-bedrock
- amazon-nova
- cp-sat
- cryptography
- fastapi
- gmail-api
- google-oauth
- matplotlib
- mcp
- mcp-apps
- or-tools
- postgresql
- pydantic
- pytest
- python
- sqlalchemy
- sqlite
- starlette
- strands-agents
- uvicorn
- web-speech-api
- websockets
Log in or sign up for Devpost to join the conversation.