> ## Documentation Index
> Fetch the complete documentation index at: https://docs.illa.io/llms.txt
> Use this file to discover all available pages before exploring further.

# PlanStream

> Execute and steer approved execution plans with executePlan, sendStepDecision, and PlanStream

`PlanStream` is the client-side wrapper for consuming planning lifecycle events while an
approved execution plan runs. Use it together with the `IllaSDK` facade methods
`executePlan` (start streaming a plan) and `sendStepDecision` (steer a step that needs a decision).

<Warning>
  Planning is an early-access capability. The planning backend is gated on the server side and
  may not be enabled for your API key or environment — when it is not, a chat turn behaves like a
  normal (non-planning) response and no `plan` is returned. The planning types and event surface
  may still change; for example, step decisions currently reuse the `/api/v1/chat/` route pending a
  dedicated endpoint.
</Warning>

## How planning fits together

1. A planning-enabled chat turn returns an `ExecutionPlan` on the response (`response.data.plan`),
   along with `requiresApproval` and `availableActions`.
2. After the user approves, call `executePlan(plan)` to stream the plan's execution.
3. Attach handlers to the returned `PlanStream` to observe lifecycle events.
4. If a step fails or needs guidance, call `sendStepDecision(...)` to retry, stop, or edit the request.

## `executePlan`

```ts theme={null}
executePlan(
  plan: ExecutionPlan,
  options?: { signal?: AbortSignal },
): Promise<PlanStream>
```

Submits an approved `ExecutionPlan` and returns a `PlanStream` that emits real-time planning
events. Internally this opens an SSE request (`Accept: text/event-stream`) and hands the response
body to a new `PlanStream`.

## `sendStepDecision`

```ts theme={null}
sendStepDecision(
  decision: StepDecision,
  options?: { signal?: AbortSignal },
): Promise<CoreApiChatResponse>
```

Sends a step-level decision. `StepDecision` has:

<ParamField body="planId" type="string" required>
  ID of the plan the step belongs to.
</ParamField>

<ParamField body="planVersion" type="number" required>
  Version of the plan (positive integer).
</ParamField>

<ParamField body="stepNumber" type="number" required>
  1-based step number the decision applies to.
</ParamField>

<ParamField body="action" type="'retry' | 'stop' | 'edit_request'" required>
  What to do with the step.
</ParamField>

<ParamField body="editedIntent" type="string">
  New intent text. Required when `action` is `'edit_request'`.
</ParamField>

The returned `CoreApiChatResponse` is the same union a chat turn returns. Narrow success with
`"status" in response && response.status === 200`.

## `PlanStream`

`PlanStream` parses SSE frames from a `ReadableStream<Uint8Array>`, validates each frame against
the shared telemetry-event schema, and dispatches planning events to typed handlers. Unknown event
types are silently ignored for forward compatibility, and stream consumption is deferred to the
next microtask so you can register handlers synchronously right after construction.

```ts theme={null}
new PlanStream(stream: ReadableStream<Uint8Array>, options?: PlanStreamOptions)
```

`PlanStreamOptions`:

<ParamField body="onClose" type="() => void">
  Called when the stream closes normally.
</ParamField>

<ParamField body="onError" type="(error: Error) => void">
  Called on a connection- or parsing-level error.
</ParamField>

### Methods

```ts theme={null}
on<T>(eventType: T, handler: (event) => void): void   // register a handler (multiple allowed per type)
off<T>(eventType: T, handler): void                   // remove a handler
close(): void                                         // abort the stream and release resources
isClosed(): boolean                                   // whether the stream has closed
```

<Note>
  The stream returned by `executePlan` is constructed without `PlanStreamOptions`, so it reports
  server-sent problems through the `error` event and closes automatically when the stream ends. To
  observe connection- or parsing-level failures (network drop, malformed SSE) or to run a close hook,
  construct `PlanStream` directly with `onError` / `onClose`.
</Note>

## Plan event vocabulary

`on()` accepts every planning event plus the stream-control types `connected`, `stream_end`,
and `error`.

**Lifecycle events**

* `plan_generating` — plan generation started (`messageCount`)
* `plan_generated` — plan ready (`planId`, `version`, `stepCount`, `complexity`, `isExecutable`, `autoExecutable`, `hasConflicts`)
* `plan_step_started` — a step began (`planId`, `stepNumber`, `description`, `operationType`)
* `plan_step_retrying` — a step attempt failed and a retry is scheduled (`stepNumber`, `attempt`, `maxRetries`, `phase`)
* `plan_step_completed` — a step finished (`stepNumber`, `status`: `completed` | `failed` | `skipped`)
* `plan_invalidated` — the plan can no longer continue (`reason`, `failedStepNumber`)
* `replan_started` — re-planning started (`previousPlanId`, `reason`, `preservedStepCount`)
* `replan_complete` — re-planning produced a new plan (`previousPlanId`, `newPlanId`, `newVersion`)
* `plan_execution_complete` — execution finished (`status`: `complete` | `partial` | `failed`, `completedSteps`, `totalSteps`)
* `plan_expired` — the plan or planning session expired before the next resume step (`subject`, `blockedAction`, `expiredAt`)

**Runtime (simulation) events**

* `plan_step_simulated` — a write step was simulated (`simulationSuccess`, `simulationSummary`)
* `plan_step_verification_passed` — the verifier accepted the simulation (`confidence`, `reason`)
* `plan_step_verification_failed` — the verifier rejected the simulation (`confidence`, `reason`, `conflicts`, `recommendedAction`)
* `plan_execution_failed` — execution failed fatally (`error`, `phase`)

## `PlanStreamHandlerFailure`

If one of your registered handlers throws while processing an event, `PlanStream` wraps the thrown
error in a `PlanStreamHandlerFailure` and forwards it to `onError`. The failure exposes the
`eventType` whose handler threw and preserves the original error as its `cause`, so a handler
throwing never breaks the stream or other handlers.

## Full example

```ts theme={null}
import { IllaSDK } from '@illalabs/sdk'
import type { ExecutionPlan } from '@illalabs/sdk'

const sdk = new IllaSDK({ apiKey: process.env.ILLA_API_KEY! })

// 1. A planning-enabled chat turn returns a plan to approve.
const turn = await sdk.sendMessage(
  'Swap 1 ETH to USDC, then supply the USDC to Aave on Base',
  { userContext: { address: '0x1234...' } },
)

if (turn.response.isError) {
  throw new Error(turn.response.error.message)
}

const plan: ExecutionPlan | undefined = turn.response.data.plan

if (!plan) {
  // Planning was not enabled for this key/environment — treat as a normal chat turn.
  console.log(turn.response.data.text)
} else {
  // 2. After the user approves, stream the plan's execution.
  const stream = await sdk.executePlan(plan)

  stream.on('plan_generated', (event) => {
    console.log(`Plan ${event.data.planId}: ${event.data.stepCount} step(s)`)
  })

  stream.on('plan_step_started', (event) => {
    console.log(`Step ${event.data.stepNumber}: ${event.data.description}`)
  })

  stream.on('plan_step_completed', (event) => {
    console.log(`Step ${event.data.stepNumber} → ${event.data.status}`)

    // 3. If a step fails, decide how to proceed.
    if (event.data.status === 'failed') {
      void sdk.sendStepDecision({
        planId: event.data.planId,
        planVersion: plan.version,
        stepNumber: event.data.stepNumber,
        action: 'retry',
      })
    }
  })

  stream.on('plan_execution_complete', (event) => {
    console.log(
      `Execution ${event.data.status}: ${event.data.completedSteps}/${event.data.totalSteps}`,
    )
    stream.close()
  })

  stream.on('error', (event) => {
    console.error('Plan error:', event.data.message)
    stream.close()
  })
}
```

## Related pages

* [ILLA SDK](/sdk/api/illa-sdk)
* [Streaming Events](/sdk/api/streaming-events)
* [TelemetryClient](/sdk/api/telemetry-client)
