Skip to content

handoff_agent example: hand it a task and walk away - #293

Merged
yuanhao merged 10 commits into
mainfrom
feat/handoff-agent
Oct 11, 2026
Merged

yuanhao merged 10 commits into
mainfrom
feat/handoff-agent

Conversation

@yuanhao

@yuanhao yuanhao commented Oct 11, 2026

Copy link
Copy Markdown
Collaborator

A new example beside coding_agent, focused on one idea: a personal handoff agent. You give it a task and leave; it works, asks you only when it must (without waiting for you), and hands back a report.

cd my-project
path/to/yoagent/scripts/try-handoff.sh --live "move the config to TOML, keep the tests green"
path/to/yoagent/scripts/try-handoff.sh --phone --live "…"   # serve on the LAN, for a phone

What it does

  • Plans: with a pi extension used unchanged, @juicesharp/rpiv-todo (207k downloads a month). A new todo-plan UI plugin draws the plan as a checklist in the page.
  • Researches and works: DSH's free web search, plus yoagent's file and shell tools in the current directory.
  • Asks without waiting (ask_owner):
    • The question goes to every frontend and the tool returns at once, so the agent carries on with independent work.
    • The answer comes back as a steer message: picked up mid-run, or starting a new run if the agent had stopped.
    • A dismissed question comes back as "skipped"; the agent decides itself and says so.
  • Guardrails:
    • pi's approval for dangerous commands. Those do wait, and with nobody connected they're refused.
    • require_policy.
    • Turn and minute limits.
    • A dollar Budget when the model is priced (DeepSeek isn't in the bundled table, so it gets turns and minutes only).
  • Handoff report:
    • A verifier sends the agent back until the run ends with Done / Waiting on you / Decisions / Next.
    • The host then adds facts the model can't make up: the change since the handoff started (git diff --shortstat against the starting commit, plus the number of commits), tokens, tool calls, time, and open questions.

One library addition: Session::ask_later

  • ask gives up at once when no frontend is attached. ask_later keeps the question open until it's answered, and a frontend that connects later gets it in its Hello.
  • While doing this I found that an ended session left its open questions pending. It now ends them with their default, and a question asked on an ended session is refused. Two new session tests cover both.

Tried live

DeepSeek, a phone-sized headless Chrome, and a scratch Python project ("move the config from JSON to TOML, keep python3 -m unittest passing"). The agent started before any browser connected.

A first-run flaw is fixed in this PR: the facts compared against the current HEAD, which hides the agent's own commits. They now compare against the commit the handoff started from.

Checks

  • --demo (scripted: plan, a question, a report; then the answer starts run 2) self-checks and runs in CI (frontend-experimental).
  • All 38 frontend tests pass with the runtimes required; clippy, docs and fmt are clean.
  • The lockfile only gained entries (rpiv-todo and rpiv-i18n 2.12.0, installed without pi's peers, which the adapter provides).

🤖 Generated with Claude Code

https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG

yuanhao and others added 10 commits October 11, 2026 11:35
`ask` answers with the default when no frontend is attached, right for an
interactive agent and wrong for one working while its owner is away.
`ask_later` keeps the question open, with no timeout, even with nobody
connected: a frontend that connects later gets it in its Hello. The
session's end now ends open questions with their default (they were left
pending), and a question asked on an ended session is refused at once.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG
A personal handoff agent on the frontend layer: it plans with the pi
extension @juicesharp/rpiv-todo (unchanged; the todo-plan UI plugin draws
it), researches with DSH's free search, asks the owner without waiting
(`ask_owner`: the answer comes back as a steer message, mid-run or
starting the next run), and ends every run with a handoff report the host
completes with facts (change since the handoff, tokens, time, open
questions). Guardrails: pi's approval of dangerous commands (refused with
nobody connected), require_policy, turn/minute limits, a dollar budget
for priced models. scripts/try-handoff.sh runs it in the current
directory. The scripted --demo runs in CI; tried live with DeepSeek on a
phone-sized page.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG
… starts the next run

From the review of #293:
- plugins/.npmrc `legacy-peer-deps=true`: the lock leaves out the pi
  packages rpiv-todo lists as peers (the pi adapter supplies them), and a
  plain `npm ci` — CI, try-coding-agent.sh, try-handoff.sh — refused it.
- The Driver now turns steering / follow-ups the loop never read (they
  arrived after its last look, e.g. while extensions finished) into the
  next run's prompt, unless the run was aborted; a reset clears them. An
  owner's answer arriving just as the agent stopped used to sit until some
  unrelated later run. Test: a_steer_arriving_as_a_run_ends_starts_the_next_run
  (fails without the fix).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG
…w files

- The page showed every question as a modal dialog, locking the whole page
  (a pi approval included) until answered. It is now docked above the
  composer and non-modal: the log can be read and scrolled, and the agent
  can be typed to, while a question waits; "1 of N questions waiting"
  when more queue behind it. Approvals still hold the agent, by design.
- handoff_agent's facts counted every untracked file as new, including
  ones there before the handoff. They now count only files new since it
  started, and name them (up to three), so a test run's bytecode shows as
  what it is.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG
…g one

The system prompt no longer assumes code: research, writing, planning or
code; what it makes goes into files in the working directory (research
with sources and how sure it is); it asks only what is the owner's to
decide (preferences, budgets, taste); nothing irreversible or done on the
owner's behalf (deleting, spending, sending, signing up) without their
answer. README and script examples use a research task. Tried live: a
headphones research task asked budget and comfort-vs-sound without
waiting, used the answers as they came, and wrote a sourced report.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG
…ed" report at info

- The session keeps its latest runs' messages (runStart, event, runEnd,
  notice; up to HISTORY = 5000) and replays them to a frontend that
  connects, after its Hello. Recorded under the clients lock, so a late
  frontend gets each message once (replay or live). For the handoff
  agent, opening the page after the agent worked shows the work, the
  report and the facts, not only the open question. Tests: a late frontend
  gets the runs so far, then live messages once; a frontend connecting
  after the driver died gets that run, then Closed.
- The pi adapter reports what does not map (app commands, shortcuts,
  unfired events) at info: expected, not a fault — a WARN at every start of
  the handoff agent (rpiv-todo's /todos, its shortcut) read as a problem.
  Refusals stay warnings. pi_log_test and the matrix's classifier follow.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG
… says when a limit stopped it

From a live research run that downloaded 13 raw HTML pages into the
owner's folder and hit yoagent's 1M-token default before writing its
report:
- The prompt names a scratch folder ($TMPDIR/yoagent-handoff-<pid>) for
  downloads and helper scripts; only deliverables go in the working
  directory; the search tools (extracted text), not whole pages; the
  deliverable written early and kept up to date.
- --max-tokens per run, 4M by default (research reads a lot; the default
  counts the whole prompt each turn); 80 turns.
- A run a limit stopped gets a warning: "Stopped early: … Send "continue"".
- A repository with no commit yet compares against git's empty tree (it
  read "not a git repository").
Re-run live: only headphones.md in the folder, 653k tokens, the question
asked and answered while the agent kept going.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG
…es; reset withdraws held questions; honest facts and demo

From review feedback on #293:
- [P1] confirm-dangerous.ts asked only for `rm -r`, sudo, `chmod -R`,
  `git push --force`, mkfs and dd: `rm -f`, `git reset --hard`, `git
  clean -fd`, shell overwrites and file writes over existing files went
  through. It now asks for anything that could destroy or overwrite what
  the owner has (rm/unlink/rmdir, globs always; mv/cp/`>` onto an existing
  file; writes over one; reset --hard, clean, checkout --/restore, branch
  -D, forced push, find -delete, truncate, shred, …). Files the agent
  created this session and the scratch folder named in YOAGENT_SCRATCH
  stay free (so the deliverable can be kept up to date); paths compared
  via realpath. Checked against 19 commands in and outside the temp dir.
- [P2] A reset left owner questions open: a late answer started a run
  with no context. `ask_later` now returns `Option` (None: withdrawn or
  the session ended) and a reset withdraws the questions held this way;
  ask_owner sends nothing on None. Test added.
- [P2] The facts diffed against the starting commit, counting edits the
  owner had not committed. They now diff against a `git stash create`
  snapshot of the working tree (files, index and stash list untouched);
  commits are still counted from the starting HEAD.
- The scripted demo's reports claimed a migration it never did. The demo
  now does what it reports: run 2 writes notes.md in the format the owner
  chose; the demo checks the file and that the facts name it (CI too).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG
@yuanhao
yuanhao merged commit 5a560b8 into main Oct 11, 2026
17 checks passed
@yuanhao
yuanhao deleted the feat/handoff-agent branch October 11, 2026 13:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant