Skip to content

feat: Sequences #15

Description

@ofriw

Dynamic TUI Sequence Design

Summary

pi-agenticoding adds a transient-looking but session-backed sequence recorder. Users define a sequence interactively in the TUI by entering steps through a prompt-like editor. The extension captures raw user-authored steps, validates them immediately, previews them exactly as typed, and runs the sequence for a fixed number of iterations.

Sequences are not config resources and are not project/global workflows. They are persisted as extension-owned custom entries on pi's session tree so the latest sequence can be restored after reload, resume, and session replacement. Restore is branch-aware: the current branch is authoritative.

Between iterations, the runner uses ctx.newSession() so notebook, readonly, handoff, child sessions, topic, and watchdog state reset normally. Only serialized sequence run progress crosses the reset boundary.

Scope

Supported

  • TUI sequence recording mode.
  • Record, validate, preview, edit, and run a sequence.
  • Restore the latest sequence snapshot from session history.
  • Fixed run count, asked at run start.
  • Free-text prompt steps.
  • pi-agenticoding commands: /readonly, /handoff, /notebook.
  • Prompt templates and skills when they can be resolved from pi.getCommands().
  • /compact through ctx.compact().
  • Full reset between iterations through ctx.newSession().

Not supported

  • Durable project/global sequence storage.
  • Third-party extension command execution.
  • UI-only built-ins such as /settings, /tree, /model.
  • Literal /new as a user-authored step. Reset is runner-owned.
  • Replaying pre-expanded template or skill text from stored sequence state.

Persistence model

Sequences use pi's session tree as the durable source of truth.

  • Persist sequence snapshots as extension-owned custom entries.
  • Rehydrate in-memory sequence state on session_start by scanning the current branch newest-to-oldest.
  • The latest snapshot on the current branch wins.
  • Sequence persistence is session-scoped, not process-scoped, and not config-scoped.
  • Sequence entries do not enter LLM context.

Suggested custom entry shape:

interface SequenceSnapshotEntry {
  version: 1;
  sequenceId: string;
  steps: string[];
  updatedAt: number;
}

Restore is branch-only in v1: always use the latest snapshot on the current branch.

User experience

Start:

/sequence

If a previous sequence snapshot exists on the current branch:

Restore last sequence?
- Run
- Edit
- Start new
- Cancel

Recording mode replaces the editor with a sequence recorder that preserves prompt-like editing and autocomplete where possible.

Footer/status:

● recording sequence · 3 steps · Enter adds step · Ctrl+Enter finishes · Esc cancels

Preview widget:

Sequence
1. /readonly
2. Plan the implementation for $@
3. /handoff execution

Preview always shows the raw authored step text. Slash commands remain visible and unexpanded so the sequence stays readable and understandable in the TUI.

On finish:

Run this sequence how many times? [1]

During execution:

⟳ sequence · run 2/5 · step 3/4 · /handoff execution

Unsupported entries fail visibly during recording:

Unsupported step: /publish is an external extension command.
pi-agenticoding cannot execute third-party extension command handlers.

Data model

Canonical persisted model

Persist only raw user-authored steps.

interface RecordedSequence {
  version: 1;
  id: string;
  steps: string[]; // exact authored input, one step per entry
  updatedAt: number;
}

Raw step text is the only sequence source of truth because command/template/skill expansion can change between runs, reloads, and replacement sessions.

Derived resolved model

Resolve raw steps into typed runtime steps during validation and execution.

type ResolvedStep =
  | { kind: "prompt"; raw: string; text: string }
  | { kind: "agenticodingCommand"; raw: string; name: "readonly" | "handoff" | "notebook"; args: string }
  | { kind: "template"; raw: string; name: string; args: string; sourcePath: string }
  | { kind: "skill"; raw: string; name: string; args: string; sourcePath: string }
  | { kind: "compact"; raw: string; instructions?: string };

Rules:

  • Preview uses raw.
  • Execution uses freshly resolved data.
  • Templates and skills are never persisted in expanded form.
  • Unsupported classes are rejected during recording and not stored unless rewritten as plain prompts.

Transient run progress

Only serialized run progress crosses ctx.newSession() boundaries.

interface SequenceRun {
  id: string;
  sequenceId: string;
  steps: string[];
  args: string[];
  totalRuns: number;
  runIndex: number;
  stepIndex: number;
}

State design

Sequence state must be centralized inside the extension state, not spread across ad hoc module singletons.

interface SequenceState {
  lastRecorded: RecordedSequence | null; // rehydrated from custom entries
  activeRun: SequenceRun | null;         // transient, cleared when run finishes
}

Guidelines:

  • Session history is the durable source of truth.
  • In-memory state is a cache of the latest rehydrated snapshot plus active transient execution state.
  • Avoid storing livePi, liveState, or similar mutable runtime mirrors as primary architecture.
  • If execution needs fresh session-bound capabilities after ctx.newSession(), reacquire them from the replacement-session context instead of caching old ones.

Rehydration

On session_start:

  1. Read the current branch through ctx.sessionManager.getBranch().
  2. Scan newest-to-oldest for sequence snapshot custom entries.
  3. Rebuild SequenceState.lastRecorded from the newest valid snapshot.
  4. Clear stale in-memory sequence state when no persisted snapshot exists on the branch.

This follows the same event-sourced persistence pattern already used for notebook state.

Validation

Validation is two-phase.

Recording-time validation

Each entered step is validated immediately.

Allowed:

  • Plain prompt text.
  • /readonly, /handoff, /notebook.
  • /compact.
  • Prompt templates from pi.getCommands() where source === "prompt".
  • Skills from pi.getCommands() where source === "skill".

Rejected:

  • Third-party extension commands.
  • Interactive-only built-ins.
  • /new.
  • Any unresolved or malformed slash command.

Runtime validation

Revalidate before execution, and re-resolve before each iteration.

Why:

  • templates/skills may change on disk
  • available commands may change after reload or session replacement
  • command provenance may differ between runs

pi.getCommands() is the canonical source of command metadata. Use source and sourceInfo instead of command-name heuristics or path guessing.

Execution rules

  1. Validate and resolve the complete sequence before starting a run.
  2. Execute steps sequentially.
  3. Before each iteration, resolve raw steps again from current command metadata.
  4. After prompt-like steps, wait for idle.
  5. /readonly, /handoff, and /notebook call shared pi-agenticoding command functions.
  6. /compact wraps ctx.compact() callbacks in a Promise.
  7. After the final step of an iteration, call ctx.newSession({ withSession }) if more iterations remain.
  8. Continue only through the fresh ReplacedSessionContext passed to withSession.
  9. Carry only SequenceRun data across reset.
  10. Abort visibly if a previously valid step can no longer be resolved.

Session replacement rules

ctx.newSession() is a hard session-boundary.

  • The old command ctx, old pi, old sessionManager, and any previously captured session-bound objects are stale after replacement.
  • withSession runs only after the replacement session is active and the new extension instance has already started.
  • Code after replacement must use only the fresh ctx passed to withSession.
  • Capture only plain serialized data across the boundary, such as SequenceRun.

This avoids stale-context bugs and keeps sequence iteration reset semantics aligned with normal pi behavior.

Command refactor

Slash handlers and sequences must share behavior:

/readonly  -> runReadonlyToggle(ctx, state)
/handoff   -> requestHandoff(ctx, state, direction)
/notebook  -> runNotebookCommand(ctx, state, args)
/sequence  -> runSequence(ctx, args)

Sequence execution must call the same shared command functions used by manual slash invocation. Do not synthesize command execution by sending /cmd text back through the prompt path.

Audit checklist

  • No pi internals imported or patched.
  • No fake command execution via pi.sendUserMessage("/cmd").
  • Sequence snapshots persist through custom session entries only.
  • Rehydration restores the latest sequence snapshot from the current branch.
  • Preview shows raw authored steps, not expanded template/skill content.
  • Runtime always revalidates dynamic commands before execution.
  • No stale ctx, pi, sessionManager, or cached session-bound objects survive ctx.newSession().
  • Unsupported commands fail before execution.
  • Notebook reset semantics remain untouched.
  • Failures are visible through recorder UI/status notifications.

Sequence snapshots remain invisible implementation details in session history and are not labeled for /tree inspection.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels
    No fields configured for Feature.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions