Skip to content

WIP: Wizard program/agent split #1320

Closed

Description

@gewenyu99

Wizard refactor

Today the agent is tightly coupled to the store and to programs. We're splitting them apart. Then you can run a single program headlessly or from code, or import the agent on its own and pass in the bindings and prompts, without touching the rest of the wizard. It also lets us split the code better.

What A changes

Image Image
  • A draws a boundary around the agent: one exported interface, so the agent is a clean module that isn't tightly coupled to the store.
  • The agent gets everything it needs to run from the program's binding, passed in directly.
  • It doesn't touch the store. When it needs to surface something to the user, it goes through interaction callbacks.

What B changes

Image Image
  • B separates the wizard TUI, with its store and screen logic, from the logic that just runs a program.
  • That makes room for the following, which are part of C:
    • Running a program directly.
    • Clean boundaries between programs.
    • Running the agent directly.

What C changes

Image Image
  • C gives each host its own state and one way in: the TUI, headless and embedders all call runProgram. The old adapter goes away.
  • Other agents and tests can call runAgent directly.
  • Every layer gets its own folder, and the compiler enforces both fences.

🤖 Generated with Claude Code

Activity

  1. gewenyu99 commented on Sep 23, 2026

    @gewenyu99
    CollaboratorAuthor

    Stack A

    • #1293 A1′ functional runner contracts
    • #1297 A2a cut the agent's outside edges and fence them
    • #1299 A2b move agent and shared source trees
    • #1303 A3 publish the agent through entry modules

    By the top of the stack, the agent lives in src/agent and is reachable only through two entries. Deeper imports fail lint and the architecture test.

    The new agent surface, @agent

    runAgent(config: RunConfig, input: RunInput, options?: {
      onProgress?: (event: AgentProgress) => unknown; // every event, in order, never awaited
      interaction?: AgentInteraction;                 // answers questions; absent means no asks
      signal?: AbortSignal;
    }): Promise<RunResult>

    Also RunOutcome, AgentSignals (the marker strings program prompts embed) and WIZARD_TOOL_NAMES (tool ids for allowedTools and disallowedTools).

    Types and interfaces, @agent/types

    • Run contract: RunConfig, RunInput, RunResult, AgentRunDefinition, PromptContext, AgentFailure, AbortCase
    • Progress and interaction: AgentProgress, AgentInteraction, AskQuestion, AskAnswers, PendingQuestion, TaskNotice, OutroData, SpinnerHandle, TokenUsageDelta, AuthErrorDetail

    Temporary. These are also exported today so the stack can reach main quickly, and will be deleted later: resolveBinding, shouldDisableAsk, LONGER_ASK_TIMEOUT_MS, ProgramBinding, SwitchboardCtx, initializeAgent, executeAgent, buildRunTags, AgentErrorType, configureGatewayFromCIEnvironment, flushScanReport, downloadSkill, InstallSkillResult, runMcpPromptViaSdk, AgentChunk.

  2. gewenyu99 commented on Sep 23, 2026

    @gewenyu99
    CollaboratorAuthor

    Four ways to call the Wizard

    Target shape after Release C. Only the calling surfaces; everything behind them is left out.

    Solid arrows are calls, dashed arrows are returns, and open-headed arrows are progress events nobody waits on. (optional) marks a step that only happens when the host supplies it or the agent needs it, and (loop) marks a step that repeats for each agent run.

    1. Full program in the TUI

    sequenceDiagram
      actor U as User
      participant CLI as src/cli
      participant T as src/tui
      participant P as runProgram<br/>@programs
      participant A as runAgent<br/>@agent
    
      U->>CLI: wizard [program]
      CLI->>T: LaunchOptions
      T->>P: runProgram(programId, input, options)
      P->>T: credentials.resolve(programId)
      T-->>P: ResolvedProgramCredentials
      P->>T: (optional) workflow.confirmStep(step)
      T->>U: gate screen
      U-->>T: decision
      T-->>P: decision
      P->>A: (loop, each agent run) runAgent(config, input, { onProgress, interaction, signal })
      A--)P: (loop) onProgress(AgentProgress)
      P--)T: (loop) onProgress(ProgramProgress), reduced into TuiStore
      A->>T: (optional) interaction.ask(question, { signal }), passed through
      T->>U: WizardAsk screen
      U-->>T: answer
      T-->>A: AskAnswers
      A-->>P: RunResult
      P-->>T: ProgramRunOutcome
      T->>U: outro, exit code
    
    Loading
    • The TUI passes an OAuth credentials provider, the WizardAsk screen as the answerer, its gates as the workflow connector, onProgress and signal.
    • Ctrl-C aborts signal, which cancels pending questions and the run.

    2. Headless

    sequenceDiagram
      actor CI as CI runner
      participant CLI as src/cli
      participant H as src/headless
      participant S as control client<br/>(optional socket)
      participant P as runProgram<br/>@programs
      participant A as runAgent<br/>@agent
    
      CI->>CLI: wizard --ci [--partial-control | --full-control]
      CLI->>H: LaunchOptions
      H->>P: runProgram(programId, input, options)
      P->>H: credentials.resolve(programId)
      H-->>P: ResolvedProgramCredentials
      P->>A: (loop, each agent run) runAgent(config, input, { onProgress, interaction, signal })
      A--)P: (loop) onProgress(AgentProgress)
      P--)H: (loop) onProgress(ProgramProgress), logged
      A->>H: (optional) interaction.ask(question, { signal }), passed through
      H->>S: (remote only) pending question
      S-->>H: answer
      H-->>A: AskAnswers, from the socket or a fixture
      S->>H: (optional) GET state
      H-->>S: headless state
      A-->>P: RunResult
      P-->>H: ProgramRunOutcome
      H->>CI: exit code, final log line
    
    Loading
    • Headless passes an API-key credentials provider, an answerer that is none, scripted or remote, no workflow connector, onProgress and signal.
    • With no answerer, the ask tool returns its unavailable error, never consent.
    • SIGINT or SIGTERM aborts signal and exits 130 or 143.

    3. A program from code

    sequenceDiagram
      participant C as caller<br/>(host program, harness)
      participant P as runProgram<br/>@programs
      participant A as runAgent<br/>@agent
    
      C->>P: runProgram(programId, input, options?)
      P->>C: (optional) credentials.resolve(programId)
      C-->>P: ResolvedProgramCredentials
      P->>C: (optional) workflow.confirmStep(step)
      C-->>P: decision
      P->>A: (loop, each agent run) runAgent(config, input, { onProgress, interaction, signal })
      A--)P: (optional, loop) onProgress(AgentProgress)
      P--)C: (optional, loop) onProgress(ProgramProgress)
      A->>C: (optional) interaction.ask(question, { signal })
      C-->>A: AskAnswers
      A-->>P: RunResult
      P-->>C: ProgramRunOutcome, no store handle
    
    Loading

    4. The agent directly

    sequenceDiagram
      participant C as caller<br/>(test, skill-only run, another agent)
      participant A as runAgent<br/>@agent
      C->>A: runAgent(config, input, options?)
      A--)C: (optional, loop) onProgress(AgentProgress)
      A->>C: (optional) interaction.ask(question, { signal })
      C-->>A: AskAnswers
      A-->>C: RunResult, complete on its own
    
    Loading
    • The caller builds RunConfig and RunInput by hand. The agent's default binding is exported as data.
    • There's no program layer, so no detection, credential lookup, gates or composition.

    Interfaces

    @programs

    runProgram(
      programId: string,
      input: ProgramInput,
      options?: {
        credentials?: CredentialsProvider;
        interaction?: AgentInteraction;       // passed through to the agent
        onProgress?: (event: ProgramProgress) => void;
        workflow?: ProgramWorkflowConnector;
        signal?: AbortSignal;
      },
    ): Promise<ProgramRunOutcome>
    
    type ProgramInput = {
      installDir: string;
      credentials?: ResolvedProgramCredentials; // or pass options.credentials
      // plus explicit detection choices, config overlay and launch overrides
    };
    
    type CredentialsProvider = {
      resolve(programId: string): Promise<ResolvedProgramCredentials>;
    };
    
    type ProgramWorkflowConnector = {
      // domain step data in, a decision or prepared value out; no screens or stores
      confirmStep(request: { programId; stepId; installDir }): Promise<boolean>;
    };
    
    type ProgramProgress = { runId: string; stepId?: string; event: AgentProgress };
    
    type ProgramRunOutcome = {
      programId: string;
      outcome: RunOutcome;
      runResults: RunResult[];   // one per agent run, in order
      artifacts: { reportFile?: string };
      failure?: RunResult['failure'];
    };

    @agent

    runAgent(
      config: RunConfig,
      input: RunInput,
      options?: {
        onProgress?: (event: AgentProgress) => void; // copied data, in order, never awaited
        interaction?: AgentInteraction;
        signal?: AbortSignal;
      },
    ): Promise<RunResult>
    
    type AgentInteraction = {
      ask?: (question: PendingQuestion, context: { signal: AbortSignal }) => Promise<AskAnswers>;
    };
    • RunConfig: run definition, resolved binding, commandments, tool constraints, analytics tags, health and report config.
    • RunInput: directory, resolved credentials and project, skill, flags, host settings, feature-flag data.
    • RunResult: outcome, skill, outro or failure, and the run snapshot. Never a live store.
  3. self-assigned this
    on Sep 23, 2026
  4. gewenyu99 commented on Oct 13, 2026

    @gewenyu99
    CollaboratorAuthor

    Done. Release C merged (main 28cc548 and later). Both hosts call runProgram (src/tui/run.ts, src/headless/run.ts), the run-agent-legacy adapter is gone, and each layer lives in its own folder behind per-layer tsconfig fences. Docs caught up in #1392.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions