Skip to content

Repository files navigation

Handoff — Describe it. Hand it off. It runs.

Site Docs Tests Strands AWS License

Handoff

Describe it. Hand it off. It runs.

Handoff turns a sentence — spoken or typed — into an autonomous workflow on the Strands Agents SDK, runs it on a schedule on AWS, handles what it can judge confidently, and stops to ask you only about the things it genuinely can't call. Your answer becomes a rule, so it asks less every week.

Built for the Agents for Humans Hackathon, Professional Agents track. The brief asked for an agent that runs autonomously and only surfaces when there's a real decision to make. This repository is that sentence, with the receipts.

Live site and guides https://handoff-eya.pages.dev · docs
Pitch deck (PDF) · deck (PPTX) · speaker script
Architecture diagram (AWS icons) · draw.io source · PDF · ARCHITECTURE.md
Demo video video/out/handoff-demo.mp4 · made by video/ and scripts/video/ from live footage of the real inbox — see The film

Contents

  1. The sixty-second tour
  2. What the hackathon asked for, and where it is
  3. Say it, watch it run
  4. The gate
  5. Architecture
  6. Built on Strands — with proof
  7. On AWS — with proof
  8. Three surfaces: Talk, terminal, browser
  9. Run it yourself
  10. Verification and cost
  11. Layout
  12. Licence

The sixty-second tour

Tap the orb and say: "Every weekday at eight, triage my inbox. Real asks from teammates become Linear tickets, newsletters get archived, ask me about anything unsure. Set it up and run it now."

Talk The run graph
1 · Say it. The caption fills in while you speak (Amazon Transcribe, streaming). Four tool calls later the workflow is saved, switched on, and started. 2 · Watch it run. The Strands Graph, drawn live from its own hook events: memory applied a rule, Gmail read eight, the executor handled seven and set one aside.
Decision Terminal
3 · One question. Why it stopped, in the agent's words; how sure it was; four buttons — or say "archive it". 4 · Or from the terminal. handoff run --watch streams the same events; every command takes --json.

Every screenshot in this README was taken from the running product between 13 and 15 September 2026 — the web UI through Playwright, the terminal through freeze, the AWS console state through the AWS CLI. Nothing is mocked up.


What the hackathon asked for, and where it is

Requirement How Handoff meets it Proof
Runs autonomously A Strands Graph (trigger → executor → completer) runs on a cron schedule — in-process on the desktop, EventBridge → Lambda → AgentCore Runtime in the cloud run graph · aws scheduler / lambda
Surfaces only for real decisions A BeforeToolCallEvent hook gates the one tool that changes anything; below the confidence threshold it defers, then asks once for the whole batch decision screen · hitl.py
Built on the Strands Agents SDK Agent, Graph, hooks, event.interrupt(), SessionRepository, MCPClient, 16 @tools imports proof · table below
Uses AWS Bedrock (Nova), AgentCore Runtime + Memory, Transcribe, Polly, DynamoDB, EventBridge, Lambda, ECR — all live in ap-northeast-2 handoff doctor · AgentCore · DynamoDB
Professional use Inbox triage, PR review triage, competitor pricing watch, Slack digest, meeting follow-up — shipped as templates; anything else described in a sentence workflows · discover
Learns the person Each decision becomes a narrow rule (sender, domain, or two keywords); AgentCore Memory in the cloud, JSON locally; the next run asks less decide → rule · memory
Real, not scripted Verified end-to-end on Bedrock Nova: a spoken sentence ends as an active, running workflow; 8 items, 7 handled alone, 1 escalated ask · inspect · usage

Say it, watch it run

Talk is the first page in the sidebar. The orb is a WebGL sphere whose colour follows the agent — blue idle, teal listening, violet thinking, green while a tool runs, and amber only when it is waiting on you. Tap it, hold Space, or switch on hands-free and it ends each utterance on silence and listens again after it answers. Ctrl+Space opens it from any page.

The workspace card a sentence became

What a spoken turn produces is drawn on the page from the agent's own events:

  • the workspace card when a workflow is saved — Signals (the cron and its plain-English reading), Jobs (what runs, where the line sits, where it reports), Agents (each MCP tool, the executor LLM, the completer SEND);
  • the run graph when a run starts — the real Graph, a node per tool call, with milliseconds, until it lands on DONE or NEEDS YOU;
  • the decision card when it stops — answerable with a click or a phrase.

The work panel: run graph and decision cards

Spoken decisions — archive it, file a ticket, draft a reply, leave it — are matched locally with no model round-trip. Speech streams to Amazon Transcribe over a WebSocket while you are still talking; Amazon Polly answers; Groq's Whisper and Orpheus are the second choice and the browser's own engines the floor, so nothing goes mute over a missing key.


The gate

The whole product is one file: src/handoff/graph/hooks/hitl.py.

response = event.interrupt(
    interrupt_name,
    reason=payload.model_dump(mode="json"),
)

A BeforeToolCallEvent hook sits in front of the only tool that changes anything in the outside world. On the first pass, below the confidence threshold, that line raises InterruptException and unwinds the agent loop before the tool body runs — nothing has happened to your mail. When a human answers, the same line returns their answer, and the tool carries out what they chose.

Unsure items are deferred, not interrupted: the executor finishes its pass, then finish_batch raises a single interrupt carrying all of them. You get one screen, not one interruption per question. The Graph state is serialised, so a run paused at 08:04 and answered at 11:30 is the same run — it survives a restart.

What it is not: an approval prompt on every action. An agent that interrupts on everything is just a worse inbox.


Architecture

Handoff architecture — AWS architecture icons

Official AWS Architecture Icons, drawn in draw.io: the editable source is docs/architecture.drawio (open it at app.diagrams.net), generated by scripts/make_drawio_architecture.py and exported through draw.io's own renderer. The detailed architecture document — this diagram plus the full design walkthrough — is docs/architecture.pdf; the long-form text is docs/ARCHITECTURE.md. An earlier Excalidraw version is kept alongside.

Three agents, because the jobs are different:

Agent Does Lives in
Builder / voice assistant Turns a description into a workflow config; the spoken variant saves and starts it in one turn agents/builder.py, chat/service.py, chat/voice_tools.py
Executor Runs the workflow as a Graph; decides what it may do alone graph/factory.py, graph/hooks/hitl.py
Learner Turns each human decision into a narrow, reusable rule agents/learner.py

Why Graph and not Swarm: a workflow is a fixed, auditable sequence — wake up, do the work, stop at anything unclear, close out. Graph models exactly that. Swarm models open-ended collaboration, which would make every run take a different path through the same job. For something filing tickets in your name at 8am, "different every time" is a bug.


Built on Strands — with proof

Strands versions and every import

SDK feature Where it earns its place
Agent the Builder, the voice assistant, the executor, the completer, the Learner, the chat
@tool decorator 16 custom tools, from fetch_unread_emails to activate_workflow
BeforeToolCallEvent hook + event.interrupt() the gate — graph/hooks/hitl.py; resume returns the human's answer
Batch interrupt finish_batch — one question for a whole pass
Graph + GraphBuilder + conditional edges graph/factory.py; an empty webhook never costs a model call
Graph.serialize_state a paused run survives a process restart
BeforeInvocationEvent / AfterInvocationEvent / AfterToolCallEvent graph/hooks/narrator.py — one narrator per node; the orb draws the Graph from these
SessionRepository + RepositorySessionManager chat/repository.py — chats persisted in the same store as everything else; the terminal and the browser share a session
Context variables into tools chat/voice_tools.pyactivate_workflow and start_run emit on the page's channel
MCPClient (stdio + HTTP) mcp/servers.py — Gmail, Linear, Slack, GitHub, Notion, Airtable, web
AgentTool subclass host/tools.py — a host runtime's tools as Strands tools
OpenAIModel subclass providers.py — repairs malformed tool-call JSON
OpenTelemetry tracing config.configure_observability()

Built and verified against strands-agents 1.55.1 on real models (Bedrock Nova Pro and Nova Lite, Groq qwen/qwen3.8-27b). The interrupt API is version-sensitive — event.interrupt(name, reason=…) returns the human's answer on resume — so pin the version before changing the gate.

The tests that matter (255 passing, lint clean)

make test

tests/test_hitl_gate.py drives the gate inside a real Strands agent and asserts the things that actually matter: an unsure call stops the loop and the tool never runs; resuming executes what the human chose, not what the agent suggested; "leave it" cancels the tool; a malformed confidence score fails toward asking, never toward acting. tests/test_orb.py runs a whole spoken turn on the scripted model and asserts it ends active and running.


On AWS — with proof

Everything below is live in ap-northeast-2, captured with the AWS CLI on 2026‑09‑13 (account id masked).

Service Purpose Proof
Amazon Bedrock — Nova Pro / Nova Lite reasoning for every agent, through cross-region inference profiles inference profiles + doctor
AgentCore Runtime serverless background execution, arm64 container, session per run, long-running invocations list-agent-runtimes → READY
AgentCore Memory learned preferences across runs list-memories → ACTIVE
Amazon Transcribe (streaming) hears you — partial results while you speak Polly voices + doctor speech
Amazon Polly (neural) speaks back same
DynamoDB one table, pk = collection, sk = id — configs, runs, interrupts, sessions, usage, audit describe-table
EventBridge Scheduler → Lambda cron triggers; Scheduler cannot target AgentCore directly, so a twelve-line function forwards the tick schedules, function, ECR repo
ECR the runtime's image same
IAM one execution role for the runtime, one for the scheduler with a single permission infra/iam_setup.py

handoff doctor: every check is a real call

More AWS proofs
identity agentcore
dynamodb scheduler + lambda + ecr
bedrock + speech cloudflare

Deploying is a handful of boto3 scripts, no console clicking:

python infra/deploy_agentcore.py --check     # says exactly what is missing
python infra/deploy_agentcore.py             # build (arm64) → ECR → AgentCore Runtime
python infra/memory_setup.py                 # AgentCore Memory store
python infra/dynamodb_setup.py               # the single table
python infra/iam_setup.py                    # the role EventBridge Scheduler assumes
python infra/lambda_setup.py --runtime-arn … # the bridge
python infra/eventbridge_setup.py --target-arn <lambda> --role-arn <role>

Every store has a local JSON backend, so none of this is required to run, test or demo the project — only to deploy it. The site is the one thing not on AWS: static files on Cloudflare Pages.

Two things Bedrock tells you confusingly. Model ids are reached through a cross-region inference profile prefixed by geography (us., apac., eu.), and the prefix must match the calling region — Handoff resolves a bare id against AWS_REGION. And Anthropic models on Bedrock are sold through AWS Marketplace while Amazon's own are not, so an account that cannot complete a Marketplace agreement gets INVALID_PAYMENT_INSTRUMENT on Claude in every region and works on Nova. That is why the default is Nova Pro.


Three surfaces: Talk, terminal, browser

Talk and the desktop window

handoff desktop opens a native window (WebKitGTK, WebKit or WebView2 — no Electron) on the orb, remembers its size and page, records the microphone itself if the webview will not, and attaches to an already-running instance instead of starting a second scheduler.

The terminal

Every feature is a command. handoff chat streams the same assistant into your terminal and the conversation continues in the browser (one session repository); handoff build "<sentence>" --activate --run turns a sentence into an active workflow; handoff talk --mic is the orb without the orb.

handoff --help

Terminal proofs — a real run on Bedrock Nova, start to finish
run --watch pending
decide → rule inspect
ask usage
workflows agents + skills
mcp schedules + credentials
git log

The browser

Overview Activity
Workspace overview — latest runs, workflows, triggers, agents Activity — the few things it needs you for, answerable inline
Chat Run inspector
Chat — a persisted Strands session; every tool call is a card Runs — a waterfall of every model turn and tool call, with cost
Agents Tool servers
Agents — run any agent on a prompt; result, tool calls, trace Tool servers — the MCP catalogue with a live invoker
Every page (light and dark)
skills memory
usage settings
credentials schedules
discover docs
overview dark activity dark
settings dark docs dark

The platform around the gate — workspaces as workspace.yml, chat, activity, runs and inspector, agents and a workbench, tool servers, skills with version history, memory, schedules, usage, settings, a welcome wizard — is documented in the guides and in-app at /docs.

The site

https://handoff-eya.pages.dev is a framework-free static site — the landing page with the live orb, and the guides — built by site/build.py from docs/site/*.md and published to Cloudflare Pages with make site-deploy.

site site docs

Run it yourself

Just to use it, on Windows, Linux or macOS — one command, no checkout:

pipx install "handoff[bedrock,desktop,voice,web]"
handoff desktop

Linux needs GTK and WebKit from your distribution for the native window, and Windows needs nothing beyond Python. Both are spelled out, per distribution, in docs/INSTALL.md.

With nothing at all — the whole loop on a scripted model, no keys, no account:

git clone https://github.com/LSUDOKO/Handoff && cd Handoff
python3.12 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev,bedrock,desktop,web,voice]"
make demo

On AWS — Bedrock for reasoning, Transcribe and Polly for voice, one aws configure and no other keys:

cp .env.example .env        # HANDOFF_MODEL_PROVIDER=bedrock, AWS_REGION=ap-northeast-2
make doctor                 # every check makes a real call
make desktop                # native window, opens on the orb — or `make serve`

With one free key (Groq): set HANDOFF_MODEL_PROVIDER=groq and GROQ_API_KEY; Whisper hears and Orpheus speaks.

Variable Meaning
HANDOFF_MODEL_PROVIDER bedrock · groq · anthropic — identical agent code, different model object
HANDOFF_SPEECH_PROVIDER auto (AWS, then Groq, then browser) · aws · groq · browser
BEDROCK_MODEL_ID / BEDROCK_FALLBACK_MODEL_ID bare ids; the geography prefix is added for AWS_REGION
CONFIDENCE_THRESHOLD below this the gate asks instead of acting (default 0.7)
USE_MOCK_TOOLS true = the synthetic eight-message inbox; false = real MCP servers
USE_DYNAMODB / USE_AGENTCORE_MEMORY cloud state; false = local JSON

Full credential walkthrough — Gmail's own OAuth sign-in, Linear, Slack, GitHub, Notion, Airtable — in docs/SETUP.md. The CLI's --state-dir always means local storage, so a scratch run can never touch the live table.


The film

The demo video is produced, not edited by hand, so it can be re-made after any change:

  • Live footage is live. scripts/video/record_live.py launches Chromium with a fake microphone fed by a WAV of the spoken sentence, taps the real orb, and records the page while Amazon Transcribe hears it over the WebSocket, Bedrock Nova Pro runs the turn, the workspace card lands, the run graph fills in and the decision card appears. A second clip answers by voice; a third runs again with the learned rule. Every milestone is timestamped so the narration is cut against what happened.
  • Narration is Amazon Polly's generative voice, one clip per sentence (scripts/video/narrate.py); the sentence that quotes the decision's numbers is regenerated from the recorded state.
  • The composition is Remotionvideo/src: typographic scenes, the real screenshots, the Excalidraw diagram panned to what is being said, captions, a music bed. scripts/video/build_cues.py turns the recorded timelines into cut points.
  • The deck cut is the Marp deck narrated slide by slide and stitched with ffmpeg (scripts/video/deck_video.py).
python scripts/video/narrate.py <work>/audio          # Polly, per sentence
python scripts/video/record_live.py <work>/audio <work>/clips
python scripts/video/build_cues.py <work>/clips <state-dir> <work>/audio
scripts/video/prepare_assets.sh <work> && scripts/video/render.sh
python scripts/video/deck_video.py <work>/deck out/handoff-pitch-deck.mp4

The MP4s are not committed; they are uploaded with the submission.


Verification and cost

Tests 255 passing · ruff clean
Doctor Bedrock, Speech, DynamoDB, AgentCore Memory green; Gmail, Linear, Slack, GitHub, Notion, Airtable skipped until keys exist
Real-model check a spoken set-up on Nova Lite ends active and running in two of two attempts; 8 items, 7 handled alone, 1 escalated
Speech round trip Polly said a sentence, Transcribe returned it word for word
Spend a spoken turn on Nova Lite costs under a tenth of a cent; Transcribe is $0.024/min, Polly $16 per million characters; a full demo take on Nova Pro is cents

Layout

src/handoff/
├── graph/hooks/hitl.py     ⭐ the interrupt gate
├── graph/hooks/narrator.py node and tool timings on the events bus — what the orb draws
├── graph/factory.py        the Strands Graph
├── graph/nodes/            trigger, executor, classifier, gate, completer
├── agents/                 builder, executor runner, learner
├── chat/                   persisted chats, streaming service, voice tools
├── speech/                 Transcribe + Polly, Whisper + Orpheus, WAV framing — one facade
├── cli/                    every feature from the terminal, one module per command group
├── platform/               workspaces, credentials, skills, agents, workbench, MCP, usage, workspace.yml
├── web/                    FastAPI + HTMX + Jinja — the shell and every page; orb.js / talk.js / work-panel.js
├── docs.py                 the guides, rendered for the app and the site
├── host/                   embed Handoff in another agent runtime
├── workflows/              five shipped templates
├── memory/store.py         AgentCore Memory + local preferences
├── mcp/servers.py          MCP registry
├── tools/voice.py          spoken commands, matched without a model
├── providers.py            resilient OpenAI-compatible provider
├── events.py               live run feed (SSE)
├── desktop.py              native window with a microphone bridge
├── doctor.py               real-call credential checks
└── app.py                  AgentCore Runtime entrypoint

docs/       ARCHITECTURE · SETUP · DEMO · SUBMISSION · pitch/ · site/ (guides) · screens/ · architecture.excalidraw
site/       the landing page and docs build, deployed to Cloudflare Pages
infra/      AgentCore, DynamoDB, Memory, IAM, Lambda, EventBridge — boto3, no console
scripts/    architecture generator and exporter, secret guard
tests/      255 tests

Licence

Apache-2.0. See LICENSE and NOTICE.

All of it is original work built on the Strands Agents SDK. No third-party source is vendored; every dependency is installed from its own distribution and stays under its own licence. The orb's shader is adapted from DORA (MIT) and the visual language from Agent.md (MIT); both are credited in NOTICE.

About

Every weekday at 8am, triage my inbox. Real asks from teammates become Linear tickets. Newsletters get archived. Mail from my manager gets a draft reply. Anything you're not sure about, ask me.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages