loopy-loop

Child Sessions

Sometimes a session needs to hand a self-contained piece of work to its own dedicated loop — with its own goal, its own workflow set, and its own session directory — and then pick up where it left off once that work is done. That is what a child session is. A workflow requests one by dropping a JSON file into the session's child_requests/ directory; the coordinator runs the child loop to a terminal state and then resumes the parent. This page describes that contract and walks through the packaged pm_planner_dispatcher set that uses it.

The child_requests contract

A workflow requests a child session by writing a JSON file into the active session's child_requests/ directory. The request names a workflow set and a goal:

{
  "workflow_set": "inner_outer_eval",
  "goal": "A complete, self-contained implementation goal for the selected item.",
  "schema_version": 1
}

When the coordinator sees a valid request (after a successful iteration), it:

  1. Derives a goal hash and creates a new child session_id.
  2. Creates the child session directory under the parent's children/ directory, and copies the request goal into the child's goal.md.
  3. Records the child in the parent's children.json ledger with status running.
  4. Removes the request file and dispatches the child's first workflow.
  5. When the child reaches a terminal state, marks the child record complete (with its final status and stop_reason) and resumes the parent session.

The goal in the request becomes the child's source-of-truth goal, exactly as if that child had been started on its own. The child runs its named workflow_set independently — different workflows, different state, different eval checks.

Depth-first, one child at a time

v1 child execution is deliberately simple: depth-first, single-child-at-a-time.

  • A request is only picked up after a successful iteration in the parent. The coordinator does not interrupt an in-flight task to start a child.
  • Requests are processed one at a time, in sorted filename order. Exactly one child is dispatched per opportunity.
  • While a child runs, the parent is suspended. The parent resumes only after the child reaches a terminal state (goal_met, unresolvable_error, max_turns, and so on — see Success & Control for the full set).
  • Only a top-level session can spawn children. A child session cannot itself request a grandchild in v1; child requests are ignored inside a session that already has a parent. This keeps the tree one level deep and the execution order easy to reason about.

The parent tracks its children in a children.json ledger at the session root, and the child directories themselves live under children/:

.loopy_loop/sessions/<parent_session_id>/
├── children.json                       # ledger: one record per child
├── child_requests/                     # inbox for pending child requests
└── children/
    └── <child_session_id>/             # a full session directory of its own
        ├── goal.md
        ├── project_state/
        ├── iterations/
        └── ...

Each children.json record captures the child session_id, workflow_set, goal_hash, and status, plus a completed_at and stop_reason once the child finishes. That gives the parent a durable, inspectable index of every child it started and how each one ended.

Worked example: pm_planner_dispatcher

The packaged pm_planner_dispatcher workflow set is built entirely around this contract. Scaffold it with:

loopy init --template pm_planner_dispatcher

It turns the parent session into a project-management loop that never implements product code directly. Instead it plans work and delegates each concrete item to a child implementation session (typically running the inner_outer_eval set). Two workflows split the job:

planner

planner runs on start and owns the PM state and the stop decision. Each run it:

  • Treats the session goal as the source of truth and maintains PM state under project_state/ (work_items.md, current_state.md, decisions.md, child_sessions.md, and the outer-owned finished.md).
  • Reflects any updates_from_user.md content into that state, then clears the file.
  • Selects exactly one available work item for the dispatcher to hand off.
  • Reviews terminal child-session evidence and marks the item accepted or needs_rework.
  • Stops the loop only when the full session goal is satisfied — not merely because one item was accepted — by writing goal_met (or unresolvable_error) to control.json.

The planner never writes child request files, and never implements child work itself.

dispatcher

dispatcher must follow the planner and owns the child_requests/ directory. Each run it does exactly one of:

  • Writes one child request JSON file for the selected item, with a complete, self-contained implementation goal (item id/title, acceptance criteria, constraints, expected delivery evidence), then marks the item waiting_for_child.
  • Imports terminal child-session evidence — links to the child session path, finished.md, eval or goal_check results, PR/merge status — back into PM state and marks the item ready_for_review.

The dispatcher never makes the acceptance decision (planner owns that), never stops the loop, and creates at most one child request per iteration.

Together, planner → dispatcher forms the parent loop, and each dispatched child is a full inner_outer_eval session that implements and evaluates one item before control returns to the planner.

For the directory contract these sessions share, see Session Layout. For how workflow sets, scheduling, and must_follow ordering work, see Workflows.