The loop
The mechanism fits in two moves. Plan a task into steps; then run a fail-safe loop until every step is done. Each cycle picks the first step that is neither done nor failed, executes it, and persists the outcome. A cut, a crash, a restart — all re-enter there. See the first code block for the shape.
Three properties carry the whole design:
- Fail-safe by construction. State is durable and the loop is idempotent over
donesteps. Nothing completed is ever recomputed. - The judge at every tick is Pippa, not dumb Python. The Python loop is scaffolding — it holds the step list and the statuses. The intelligence (execute this step, did it succeed, what's the output, is the plan still right) is a Pippa turn at each tick.
- Each tick is one step, and a step is the unit of loss. A cut can still land mid-turn, but it costs at most the step in flight: everything committed before it stays committed.
아빠: "각 루프에서 판단하는 게 어차피 피파야. 그냥 멍때리는 python code가 아니고."
("The thing judging in each loop is Pippa anyway. It's not just dumb, spaced-out Python code.")
The right primitive: an explicit state machine
The loop is really a state-machine driver, and naming it as such turns the fail-safe property from a hope into a guarantee. This isn't a foreign import — cwkPippa already runs this exact pattern in production. The coop letters lifecycle is a 6-state machine (unread → read → working → done, with abandoned / superseded branches); cinder's JobState is queued → running → completed / failed. We adopt the house pattern.
Two nested machines. The step machine: pending → running → done, with running → blocked (an uncertain outcome, or a side effect waiting for confirmation) and running → failed (a definitive failure, or the attempt cap exceeded). A failed step is terminal for that step and never re-runs; resume skips it. A cut leaves a step in running with its attempt count bumped, re-entered on resume. The task machine: planning → running → done, with paused (Dad interrupts, or a step failed while others can still run, so the task waits for a replacement step via add_steps), blocked, failed (only when nothing runnable remains), and superseded (explicitly replaced by a successor task).
It also collapses two hard questions
Pippa's entire per-tick job reduces to committing the step's transition: done (with the output to persist), blocked (uncertain; recoverable later through unblock), or failed (definitive). That's the success-signaling question, answered. And the infinite-loop backstop becomes structural, not a bolted-on counter: a failed step never runs again, a blocked one waits for an explicit unblock, and running re-entry is attempt-capped. The machine cannot spin forever.