feat(my-deepagent): v0.2 PR #2b — mydeepagent runs resume <id> real implementation

Closes the v0.1.0 KNOWN LIMIT where resume was an exit-2 stub. Builds on
v0.2 PR #2a's LangGraph wiring + the existing DB phase-state machine +
sweep_orphan_runs — no Temporal (per DR-3).

Highlights
- `WorkflowEngine.resume(run_id)` (new async method):
  - Loads RunRow, rejects terminal states with
    MyDeepAgentError("run_already_terminal").
  - Reloads worktree_root from `RunRow.worktree_root`, template via
    `_reload_template` (WorkflowTemplateRow JOIN + model_validate), and
    bindings via `_reload_bindings` (run_bindings ⨝ agent_personas).
  - **Does NOT call `bind_personas` again** — locks in the original
    binding so consent / persona-pool changes since the original run
    don't silently shift role assignment.
- `_execute_run` (extracted shared phase loop): `run()` and `resume()`
  both dispatch through it. Skips already-completed phases (emits
  `phase.skipped` event) and re-executes the rest.
- 4 new private helpers on WorkflowEngine: `_get_run_or_raise`,
  `_reload_template`, `_reload_bindings`, `_get_completed_phase_keys`.
- `RunEventType.RUN_RESUMED` and `PHASE_SKIPPED` are now actually
  emitted (the enum members existed already).
- `cli/runs.py _runs_resume_async`: stub → real impl. Validates the run
  exists + non-terminal, loads seed personas + artifact schemas from
  `docs/schemas/`, constructs WorkflowEngine with an
  "abort-on-new-approval" callback (resume should not silently re-prompt
  the user — original gates already passed; a new gate means the
  workflow has changed). Calls engine.resume(UUID(id)), prints final
  state + report. Catches MyDeepAgentError and exits 1 with red error.

Tests
- `tests/integration/test_resume.py` (new, 5 scenarios):
  1. 2-phase mock workflow: phase 1 succeeds, phase 2 fails first time,
     row flipped back to executing → resume → phase 2 completes.
     Asserts `phase.skipped` event for phase 1, `run.resumed` event,
     and exactly 1 mock invocation for phase 2 on resume.
  2. Terminal run → `MyDeepAgentError(code="run_already_terminal")`.
  3. Unknown run id → `MyDeepAgentError(code="run_not_found")`.
  4. RunBindingRow rows missing → `MyDeepAgentError(code="run_metadata_missing")`.
  5. Corrupt `workflow_templates.definition` →
     `MyDeepAgentError(code="template_load_failed")`.
  Mock pattern matches existing test_engine.py: patch
  `my_deepagent.engine.build_agent` to return a fake agent that writes
  the expected artifact and drives the watcher middleware.

Gates
- ruff check + ruff format --check + mypy --strict: PASS (103 source files)
- pytest non-E2E: 587 PASS (12.69 s) — +5 from new resume tests
- pytest E2E real OpenRouter on Postgres: PASS 78.52 s (baseline 71–122 s;
  within DR-3 acceptance threshold ≤+20%)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
chungyeong
2026-05-16 22:07:24 +09:00
parent 50aacd3382
commit 501292a5cd
4 changed files with 804 additions and 16 deletions

View File

@@ -3,6 +3,45 @@
## [Unreleased]
### Added
- **v0.2 PR #2b`mydeepagent runs resume <id>` real implementation**.
Closes the v0.1.0 KNOWN LIMIT where resume was an exit-2 stub. Reuses
v0.2 PR #2a's LangGraph wiring + sweep_orphan_runs's DB state machine,
no Temporal (DR-3).
- `src/my_deepagent/engine.py`:
- New `WorkflowEngine.resume(run_id)` async method. Loads `RunRow`,
rejects terminal states with `MyDeepAgentError.human_required("run_already_terminal")`,
reloads `worktree_root` + `WorkflowTemplate` (via `_reload_template`) +
bindings (via `_reload_bindings`) from DB. Does **not** call
`bind_personas` again — locks in the original binding so consent /
pool changes don't silently shift roles.
- New `_execute_run` helper (shared phase loop) extracted from `run()`.
Skips already-`completed` phases (emits `phase.skipped` event) and
re-executes the rest. Both `run` (new) and `resume` dispatch through
it.
- New helpers: `_get_run_or_raise`, `_reload_template`,
`_reload_bindings` (rebuilds `{role_id: Binding}` from
`run_bindings``agent_personas`; corrupt persona rows are logged
and skipped, surfacing as `run_metadata_missing` if no bindings remain),
`_get_completed_phase_keys`.
- New `RunEventType.RUN_RESUMED` and `RunEventType.PHASE_SKIPPED` are
now actually emitted (the enum members existed already from v0.1.0).
- `src/my_deepagent/cli/runs.py` `_runs_resume_async`: stub → real impl.
Validates run exists + non-terminal, loads seed personas + artifact
schemas (`docs/schemas/`), constructs `WorkflowEngine` with a
"abort-on-new-approval" callback, calls `engine.resume(UUID(id))`,
prints final state + report path. Catches `MyDeepAgentError` and prints
a red error with exit 1.
- `tests/integration/test_resume.py` (new, 5 scenarios):
1. 2-phase workflow: phase 1 succeeds, phase 2 fails → flip run row
back to executing → resume → phase 2 completes; assert phase 1 was
skipped (`phase.skipped` event present) and `run.resumed` event emitted.
2. Terminal run → `resume()` raises `MyDeepAgentError(code="run_already_terminal")`.
3. Unknown run id → raises `MyDeepAgentError(code="run_not_found")`.
4. RunBindingRow rows missing → raises `MyDeepAgentError(code="run_metadata_missing")`.
5. workflow_templates.definition is malformed → raises `MyDeepAgentError(code="template_load_failed")`.
- E2E real OpenRouter regression PASS 78.52 s (baseline 71122 s);
within DR-3 acceptance threshold (+20%).
- **v0.2 PR #2a — LangGraph `AsyncPostgresSaver` engine wiring** (foundation
for `runs resume`). v0.2 PR #1 added the dependency; this commit actually
uses it.