Page actions

React Deep Agents Planning preview

Make an agent’s plan visible while it works. A read-only task list lets the user follow progress without treating each task label as another assistant message.

The native React APIs on this page are experimental, private source-build packages. Run the repository example to use them; they are not an npm installation recipe.

Project the plan from agent state

The graph publishes its todo list in state. React observes that data, validates each list update, and renders a separate plan panel beside the conversation. The list describes the current plan; checking a visual row is not an execution command. The example retains the last valid observation when an update is malformed and distinguishes live progress from saved completion.

The React conversation walkthrough connects this capability to requests, tools, UI state, and saved history.

Start a dispatch brief

Choose Run, type a message and choose Send or press Enter. Dispatch brief: KSFO to KASE fills the draft with the existing aviation suggestion; it does not send. Airport elevation, runway and weather lookups use fixed tables in the unchanged graph.

The first Send creates the conversation. New conversation clears the local view and waits for Send to create another conversation. A failed creation can be retried. Opening Docs, Code or Run makes no thread request. Mode navigation and browser history preserve the mounted frame and unsent draft; reloading clears local state.

app.tsx
// eslint-disable-next-line @nx/enforce-module-boundaries -- The isolated consumer copies this internal example layout.
import { ExampleFooter, ExampleLayout } from '../../../../shared/react-example';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useAgent } from '@threadplane/react';
import {
  ChatInput,
  MessageList,
  Reasoning,
  ToolObservation,
} from '@threadplane/react/chat';
import { Markdown } from '@threadplane/react/markdown';
import type { MessageRow } from '@threadplane/content/messages';
import {
  createConnectedApplication,
  type PlanningConnection,
} from './connection';
import type { PlanningApplicationSnapshot } from './application';
import { PlanPanel } from './plan-panel';
const literal = (value: unknown) =>
  typeof value === 'string' ? value : JSON.stringify(value, null, 2);
export function renderMessage(
  row: MessageRow,
  snapshot: PlanningApplicationSnapshot
) {
  const label =
    row.role === 'user'
      ? 'You'
      : row.role === 'assistant'
      ? 'Assistant'
      : row.role === 'tool'
      ? 'Tool result'
      : 'System';
  return (
    <article
      aria-label={label}
      className={`tp-chat-message tp-chat-message--${row.role}`}
    >
      <p className="tp-chat-message__role">{label}</p>
      {row.role === 'assistant' && row.reasoning && (
        <Reasoning snapshot={row.reasoning} />
      )}
      {row.role === 'tool' ? (
        <details>
          <summary>Tool result received</summary>
          <pre>{row.message.content}</pre>
        </details>
      ) : (
        <Markdown snapshot={row.markdown} />
      )}
      {snapshot.toolCalls
        .filter((call) => row.message.toolCallIds?.includes(call.id))
        .map((call) => (
          <details key={call.id} className="tool-inspection">
            <summary>
              {call.name} ·{' '}
              {call.status === 'complete' ? 'result received' : call.status}
            </summary>
            <ToolObservation
              name={call.name}
              argumentsText={literal(call.args) ?? ''}
              resultText={
                call.status === 'complete'
                  ? literal(call.result)
                  : call.status === 'error'
                  ? call.error
                  : undefined
              }
              label={
                call.status === 'complete'
                  ? 'Tool result received; plan confirmation is shown separately.'
                  : `Observed tool: ${call.status}`
              }
            />
          </details>
        ))}
    </article>
  );
}
export function connectionFingerprint(connection: PlanningConnection) {
  return JSON.stringify([
    connection.apiUrl,
    Object.entries(connection.headers).sort(([a], [b]) => a.localeCompare(b)),
  ]);
}
type Props = {
  readonly connection: PlanningConnection;
  readonly onReady: () => void;
};
export function PlanningDemo(props: Props) {
  const fingerprint = connectionFingerprint(props.connection),
    identity = useRef({ fingerprint, version: 0 });
  if (identity.current.fingerprint !== fingerprint)
    identity.current = { fingerprint, version: identity.current.version + 1 };
  return <PlanningOwner key={identity.current.version} {...props} />;
}
function PlanningOwner({ connection, onReady }: Props) {
  const [application] = useState(() => createConnectedApplication(connection)),
    [draft, setDraft] = useState(''),
    snapshot = useAgent(application);
  useEffect(() => {
    onReady();
  }, [onReady]);
  useEffect(
    () => () => {
      void application.dispose();
    },
    [application]
  );
  const renderRow = useCallback(
    (row: MessageRow) => renderMessage(row, snapshot),
    [snapshot]
  );
  const currentView = () =>
    application.getSnapshot().viewGeneration === snapshot.viewGeneration;
  const status = snapshot.busy
    ? snapshot.phase === 'confirming'
      ? 'Confirming saved plan…'
      : 'Receiving response…'
    : snapshot.phase === 'saved'
    ? 'Response saved.'
    : snapshot.phase === 'stopped'
    ? 'Response stopped.'
    : snapshot.phase === 'unconfirmed'
    ? 'Plan update unconfirmed.'
    : snapshot.phase === 'failed'
    ? 'Response failed.'
    : 'Ready.';
  return (
    <ExampleLayout className="planning-demo">
      <header>
        <div>
          <h1>A plan for the flight ahead.</h1>
        </div>
        <button
          type="button"
          onClick={() => {
            if (currentView()) {
              setDraft('');
              void application.newConversation();
            }
          }}
        >
          New conversation
        </button>
      </header>
      <p className="introduction">
        Build a dispatch brief using fixed aviation lookup data. Watch the
        assistant build and revise its plan alongside the conversation.
      </p>
      <div className="planning-layout">
        <section className="conversation-panel" aria-label="Chat">
          <div className="seed-prompts" aria-label="Example prompts">
            {[
              {
                label: 'Dispatch brief: KSFO to KASE',
                text: 'Plan a dispatch brief for a flight from KSFO to KASE: check field elevation, runway length, and weather at both ends, then tell me if there is anything the crew should know.',
              },
            ].map((prompt) => (
              <button
                key={prompt.label}
                type="button"
                disabled={snapshot.busy || !snapshot.canSubmit}
                onClick={() => {
                  const current = application.getSnapshot();
                  if (currentView() && !current.busy && current.canSubmit)
                    setDraft(prompt.text);
                }}
              >
                {prompt.label}
              </button>
            ))}
          </div>
          {!snapshot.rows.length && (
            <div className="conversation-empty">
              <span aria-hidden="true">↗</span>
              <h2>Start with what you want to do.</h2>
              <p>
                A message creates your conversation. Suggestions just fill the
                draft.
              </p>
            </div>
          )}
          <MessageList
            key={`messages:${snapshot.viewGeneration}`}
            rows={snapshot.rows}
            renderMessage={renderRow}
            label="Conversation"
          />
          <ExampleFooter>
            {snapshot.error && (
              <p role="alert" className="request-error">
                {snapshot.error}
              </p>
            )}
            <ChatInput
              key={`composer:${snapshot.viewGeneration}`}
              label="Message"
              value={draft}
              onValueChange={(text) => {
                if (currentView()) setDraft(text);
              }}
              placeholder="Ask for a dispatch brief…"
              hint="Enter to send. Shift+Enter adds a new line."
              busy={snapshot.busy}
              disabled={snapshot.busy || !snapshot.canSubmit}
              onStop={
                snapshot.busy
                  ? () => {
                      if (currentView()) void application.stop();
                    }
                  : undefined
              }
              onSubmit={(text) => {
                const current = application.getSnapshot();
                if (
                  !currentView() ||
                  current.busy ||
                  !current.canSubmit ||
                  !text.trim()
                )
                  return false;
                void application.submit(text);
                return currentView() && application.getSnapshot().busy;
              }}
            />
            <p role="status" className="response-status">
              {status}
            </p>
          </ExampleFooter>
        </section>
        <PlanPanel
          observed={snapshot.observedPlan}
          saved={snapshot.savedPlan}
          phase={snapshot.phase}
          notice={snapshot.notice}
        />
      </div>
    </ExampleLayout>
  );
}
connection.ts
import { Client } from '@langchain/langgraph-sdk';
import { createSession } from '@threadplane/langgraph';
// eslint-disable-next-line @nx/enforce-module-boundaries -- Internal bridge is copied by the isolated build.
import type { RuntimeBridgeConfiguration } from '../../../../../libs/cockpit-runtime-bridge/src/index';
import {
  createPlanningApplication,
  type PlanningClient,
  type PlanningSession,
} from './application';
import { copyJson } from './plan-state';
import { checkpointSource, type Checkpoint } from './terminal';
export interface PlanningConnection {
  readonly apiUrl: string;
  readonly headers: Record<string, string>;
}
export function planningConnection(
  configuration: RuntimeBridgeConfiguration
): PlanningConnection {
  if (
    configuration.status === 'error' ||
    (configuration.status === 'configured' &&
      configuration.target.kind === 'ag-ui')
  )
    throw Error('Runtime configuration failed.');
  const target =
    configuration.status === 'configured' &&
    configuration.target.kind === 'langsmith'
      ? configuration.target
      : null;
  return {
    apiUrl: target?.apiUrl ?? new URL('/api', window.location.origin).href,
    headers: target ? { 'x-api-key': target.apiKey } : {},
  };
}
export function createPlanningClient(
  input: PlanningConnection
): PlanningClient {
  const connection = Object.freeze({
      apiUrl: input.apiUrl,
      headers: Object.freeze({ ...input.headers }),
    }),
    known = new Set<string>();
  const client = new Client({
    apiUrl: connection.apiUrl,
    apiKey: null,
    defaultHeaders: connection.headers,
    callerOptions: { maxRetries: 0 },
  });
  function owned(threadId: string, signal: AbortSignal) {
    if (!known.has(threadId) || signal.aborted)
      throw Error('Conversation is unavailable.');
  }
  return {
    async createThread(signal) {
      try {
        if (signal.aborted) throw Error('Aborted');
        const id = crypto.randomUUID(),
          created = await client.threads.create({
            threadId: id,
            ifExists: 'raise',
            signal,
          });
        if (signal.aborted || created.thread_id !== id)
          throw Error('Unconfirmed');
        known.add(id);
        return id;
      } catch {
        throw Error('Conversation creation was not confirmed.');
      }
    },
    sessionFactory(threadId) {
      if (!known.has(threadId)) throw Error('Unknown conversation.');
      let session: ReturnType<typeof createSession> | undefined;
      try {
        session = createSession({
          assistantId: 'da-planning',
          threadId,
          apiUrl: connection.apiUrl,
          clientOptions: { maxRetries: 0, defaultHeaders: connection.headers },
        });
        const state = session.getSnapshot();
        if (
          !state ||
          !Array.isArray(state.messages) ||
          !Array.isArray(state.toolCalls) ||
          !Array.isArray(state.interrupts) ||
          !Array.isArray(state.subgraphs) ||
          ![
            'getSnapshot',
            'subscribe',
            'submit',
            'load',
            'stop',
            'dispose',
          ].every(
            (method) =>
              typeof session?.[method as keyof typeof session] === 'function'
          )
        )
          throw Error('Rejected session');
        return session as PlanningSession;
      } catch {
        try {
          void Promise.resolve(session?.dispose()).catch(() => undefined);
        } catch {
          /* Rejected session is never installed. */
        }
        throw Error('Runtime session is unavailable.');
      }
    },
    async readCurrent(threadId, signal) {
      owned(threadId, signal);
      const result = await client.threads.getState(threadId, undefined, {
        signal,
      });
      owned(threadId, signal);
      return result;
    },
    async readCheckpoint(requested, signal) {
      const captured = copyJson(requested) as Checkpoint,
        source = checkpointSource(captured, captured.thread_id);
      if (!source) throw Error('Checkpoint is unavailable.');
      owned(source.thread_id, signal);
      const result = await client.threads.getState(
        source.thread_id,
        source as Parameters<typeof client.threads.getState>[1],
        { signal }
      );
      owned(source.thread_id, signal);
      return result;
    },
  };
}
export function createConnectedApplication(connection: PlanningConnection) {
  return createPlanningApplication(createPlanningClient(connection));
}

Watch the current plan

Each valid todos update replaces the whole observed list while the response is running. The panel preserves duplicate tasks and their order, and an empty list clears it. Empty task text has a visible placeholder. Validation allows at most 50 items and 2000 UTF-16 code units per task. An invalid update retains the last valid observed plan and shows a notice; a later valid update clears that notice.

The read-only panel shows the backend's pending, in-progress and completed statuses and derives progress from them. Finishing a response does not mark unfinished tasks completed. Live observed plan and Last saved plan are separate: a visible streamed update has not yet been confirmed as saved.

plan-state.ts
export interface PlanItem {
  readonly content: string;
  readonly status: 'pending' | 'in_progress' | 'completed';
}
export type PlanState =
  | { readonly kind: 'valid'; readonly items: readonly PlanItem[] }
  | { readonly kind: 'missing' }
  | { readonly kind: 'invalid' };
 
export function plainRecord(value: unknown): value is Record<string, unknown> {
  return (
    value !== null &&
    typeof value === 'object' &&
    Object.getPrototypeOf(value) === Object.prototype
  );
}
 
/** Capture own JSON data without invoking accessors. Reject unsupported values,
 * cycles, sparse arrays and excessive depth; never coerce or truncate. */
export function copyJson(
  input: unknown,
  omitOptionalUndefined = false
): unknown {
  const path = new Set<object>();
  let nodes = 0;
  function copy(value: unknown, depth: number): unknown {
    if (++nodes > 100000 || depth > 100)
      throw new Error('Unsupported JSON size');
    if (
      value === null ||
      typeof value === 'string' ||
      typeof value === 'boolean'
    )
      return value;
    if (typeof value === 'number' && Number.isFinite(value)) return value;
    if (!Array.isArray(value) && !plainRecord(value))
      throw new Error('Unsupported JSON value');
    const object = value as object;
    if (path.has(object)) throw new Error('Cyclic data');
    path.add(object);
    const keys = Reflect.ownKeys(object);
    const array = Array.isArray(value);
    if (array && Object.getPrototypeOf(value) !== Array.prototype)
      throw new Error('Unsupported array');
    const entries = keys.filter((k) => !(array && k === 'length'));
    if (array && entries.length !== value.length)
      throw new Error('Sparse or extended array');
    const output: Record<string, unknown> = {};
    for (const key of entries) {
      if (typeof key !== 'string') throw new Error('Symbol key');
      const descriptor = Object.getOwnPropertyDescriptor(object, key);
      if (!descriptor || !('value' in descriptor) || !descriptor.enumerable)
        throw new Error('Non-JSON property');
      // Native contracts have optional own fields; raw graph evidence does not.
      if (!array && omitOptionalUndefined && descriptor.value === undefined)
        continue;
      if (array && (!/^(0|[1-9]\d*)$/.test(key) || Number(key) >= value.length))
        throw new Error('Extended array');
      Object.defineProperty(output, key, {
        value: copy(descriptor.value, depth + 1),
        enumerable: true,
      });
    }
    path.delete(object);
    return Object.freeze(
      array
        ? Array.from({ length: value.length }, (_, i) => output[String(i)])
        : output
    );
  }
  return copy(input, 0);
}
 
export function sameJson(left: unknown, right: unknown): boolean {
  if (left === right) return true;
  if (Array.isArray(left) && Array.isArray(right))
    return (
      left.length === right.length &&
      left.every((v, i) => sameJson(v, right[i]))
    );
  if (!plainRecord(left) || !plainRecord(right)) return false;
  const keys = Object.keys(left);
  return (
    keys.length === Object.keys(right).length &&
    keys.every((k) => Object.hasOwn(right, k) && sameJson(left[k], right[k]))
  );
}
 
export function planState(input: unknown): PlanState {
  if (input === undefined) return Object.freeze({ kind: 'missing' });
  try {
    const root = copyJson(input);
    if (!plainRecord(root)) return Object.freeze({ kind: 'invalid' });
    if (!Object.hasOwn(root, 'todos'))
      return Object.freeze({ kind: 'missing' });
    const todos = root.todos;
    if (
      !Array.isArray(todos) ||
      todos.length > 50 ||
      todos.some(
        (row) =>
          !plainRecord(row) ||
          Object.keys(row).length !== 2 ||
          !Object.hasOwn(row, 'content') ||
          !Object.hasOwn(row, 'status') ||
          typeof row.content !== 'string' ||
          row.content.length > 2000 ||
          !['pending', 'in_progress', 'completed'].includes(
            row.status as string
          )
      )
    )
      return Object.freeze({ kind: 'invalid' });
    return Object.freeze({
      kind: 'valid',
      items: todos as readonly PlanItem[],
    });
  } catch {
    return Object.freeze({ kind: 'invalid' });
  }
}
 
export function planProgress(items: readonly PlanItem[]) {
  return Object.freeze({
    completed: items.filter((row) => row.status === 'completed').length,
    total: items.length,
  });
}
plan-panel.tsx
import type { PlanningApplicationSnapshot } from './application';
import { planProgress, type PlanItem, type PlanState } from './plan-state';
type Valid = Exclude<PlanState, { kind: 'invalid' }>;
const labels = {
  pending: 'Pending',
  in_progress: 'In progress',
  completed: 'Completed',
};
function PlanRows({ items }: { readonly items: readonly PlanItem[] }) {
  return (
    <ol className="plan-items">
      {items.map((item, index) => (
        <li key={index} data-plan-status={item.status}>
          <span className="plan-marker" aria-hidden="true">
            {item.status === 'completed' ? '✓' : index + 1}
          </span>
          <div>
            <p>{item.content || 'Untitled item'}</p>
            <span className="plan-item-status">{labels[item.status]}</span>
          </div>
        </li>
      ))}
    </ol>
  );
}
export function PlanPanel({
  observed,
  saved,
  phase,
  notice,
}: {
  readonly observed: Valid;
  readonly saved: Valid;
  readonly phase: PlanningApplicationSnapshot['phase'];
  readonly notice: string | null;
}) {
  const progress =
    observed.kind === 'valid' ? planProgress(observed.items) : undefined;
  const status =
    phase === 'saved'
      ? 'Saved plan'
      : phase === 'working' || phase === 'confirming'
      ? 'Live plan · waiting for confirmation'
      : phase === 'idle'
      ? 'Your plan'
      : phase === 'stopped'
      ? 'Stopped · last update not confirmed'
      : phase === 'failed'
      ? 'Failed response · last update not confirmed'
      : 'Unconfirmed update · last saved plan retained';
  return (
    <aside className="plan-panel" aria-label="Plan">
      <div className="plan-heading">
        <p className="eyebrow">Plan</p>
        <h2>A clear path forward</h2>
        <p className="plan-authority">{status}</p>
      </div>
      {progress && (
        <div className="plan-progress">
          <p>
            {progress.completed} of {progress.total} completed
          </p>
          <progress
            aria-label="Plan completion"
            value={progress.completed}
            max={progress.total || 1}
          />
        </div>
      )}
      {observed.kind === 'missing' ? (
        <p className="plan-empty">
          Ask the assistant to make a plan. Its full list will appear here as it
          updates.
        </p>
      ) : observed.items.length ? (
        <PlanRows items={observed.items} />
      ) : (
        <p className="plan-empty">The plan is empty.</p>
      )}
      {notice && (
        <p role="status" className="plan-notice">
          {notice}
        </p>
      )}
      {phase !== 'idle' && (
        <p className="plan-explanation">
          Item status records the assistant’s plan. “In progress” does not mean
          a response is still running.
        </p>
      )}
      {saved.kind === 'valid' && phase !== 'saved' && (
        <details className="saved-plan">
          <summary>Last saved plan</summary>
          {saved.items.length ? (
            <PlanRows items={saved.items} />
          ) : (
            <p>The saved plan is empty.</p>
          )}
        </details>
      )}
    </aside>
  );
}

Confirm what was saved

A tool marked complete means its result was received; it does not prove the plan was applied. Tool arguments, received results and errors remain inspectable beside the conversation. Saved confirmation separately checks the raw tool result status. The example confirms a saved plan only after the actual run succeeds and its exact saved checkpoint agrees with the current human request, retained conversation prefix, canonical native messages and successful write_todos evidence. Tool arguments and checkpoint todos must match exactly, including order and duplicates. Prose and printed Python representations do not supply plan authority.

application.ts
import type {
  AgentSession,
  AgentSnapshot,
  CompleteOutcome,
  Message,
  ToolCall,
} from '@threadplane/core';
import {
  createMessageContent,
  type MessageRow,
} from '@threadplane/content/messages';
import { copyJson, plainRecord, planState, type PlanState } from './plan-state';
import {
  captureBaseline,
  captureTurn,
  captureTerminal,
  checkpointSource,
  type Checkpoint,
  type PlanningSnapshot,
  type Turn,
} from './terminal';
 
export interface PlanningSession extends Omit<AgentSession, 'getSnapshot'> {
  getSnapshot(): AgentSnapshot & PlanningSnapshot;
  load(options?: { readonly signal?: AbortSignal }): Promise<void>;
}
export interface PlanningClient {
  createThread(signal: AbortSignal): Promise<string>;
  sessionFactory(threadId: string): PlanningSession;
  readCurrent(threadId: string, signal: AbortSignal): Promise<unknown>;
  readCheckpoint(source: Checkpoint, signal: AbortSignal): Promise<unknown>;
}
export interface PlanningApplicationSnapshot {
  readonly threadId: string | null;
  readonly rows: readonly MessageRow[];
  readonly messages: readonly Message[];
  readonly toolCalls: readonly ToolCall[];
  readonly observedPlan: Exclude<PlanState, { kind: 'invalid' }>;
  readonly savedPlan: Exclude<PlanState, { kind: 'invalid' }>;
  readonly phase:
    | 'idle'
    | 'working'
    | 'confirming'
    | 'saved'
    | 'stopped'
    | 'failed'
    | 'unconfirmed';
  readonly busy: boolean;
  readonly canSubmit: boolean;
  readonly outcome: CompleteOutcome | null;
  readonly notice: string | null;
  readonly error: string | null;
  readonly viewGeneration: number;
}
const empty = (): PlanningApplicationSnapshot =>
  Object.freeze({
    threadId: null,
    rows: Object.freeze([]),
    messages: Object.freeze([]),
    toolCalls: Object.freeze([]),
    observedPlan: Object.freeze({ kind: 'missing' }),
    savedPlan: Object.freeze({ kind: 'missing' }),
    phase: 'idle',
    busy: false,
    canSubmit: true,
    outcome: null,
    notice: null,
    error: null,
    viewGeneration: 0,
  });
 
/** One lazy native owner. Live observation never grants saved authority. */
export function createPlanningApplication(client: PlanningClient) {
  type Owner = {
    id: string;
    identity: string;
    session: PlanningSession;
    content: ReturnType<typeof createMessageContent>;
    release?: () => void;
    closed: boolean;
    attempted: boolean;
    checkpointKnown: boolean;
  };
  type Attempt = {
    controller: AbortController;
    owner?: Owner;
    generation: string;
    turn?: Turn;
    baseline?: ReturnType<typeof captureBaseline>;
    text: string;
    view: number;
    submitting?: boolean;
    saving?: boolean;
    previousHumanIds?: ReadonlySet<string>;
    previousGenerations?: ReadonlySet<string>;
  };
  let snapshot = empty(),
    owner: Owner | undefined,
    attempt: Attempt | undefined,
    disposed = false,
    disposal: Promise<void> | undefined;
  const listeners = new Set<() => void>(),
    cleanups = new Set<Promise<void>>();
  const current = (run: Attempt, selected = run.owner) =>
    !disposed &&
    attempt === run &&
    !run.controller.signal.aborted &&
    snapshot.viewGeneration === run.view &&
    (!selected || (owner === selected && !selected.closed));
  function publish(update: Partial<PlanningApplicationSnapshot>) {
    if (disposed) return;
    snapshot = Object.freeze({ ...snapshot, ...update });
    for (const notify of [...listeners]) notify();
  }
  function close(selected: Owner) {
    if (selected.closed) return Promise.resolve();
    selected.closed = true;
    selected.release?.();
    selected.content.dispose();
    let timer: ReturnType<typeof setTimeout>;
    const cleanup = Promise.race([
      Promise.resolve()
        .then(() => selected.session.dispose())
        .catch(() => undefined),
      new Promise<void>((resolve) => {
        timer = setTimeout(resolve, 2000);
      }),
    ]).finally(() => clearTimeout(timer));
    cleanups.add(cleanup);
    void cleanup.then(() => cleanups.delete(cleanup));
    return cleanup;
  }
  function observe(run: Attempt): PlanningSnapshot | undefined {
    const selected = run.owner;
    if (!selected || !current(run)) return;
    const source = selected.session.getSnapshot();
    if (!current(run)) return;
    const state = copyJson(source, true) as ReturnType<
      PlanningSession['getSnapshot']
    >;
    if (
      !Array.isArray(state.messages) ||
      !Array.isArray(state.toolCalls) ||
      !Array.isArray(state.interrupts) ||
      !Array.isArray(state.subgraphs)
    )
      throw Error('Unavailable native state');
    if (!run.turn && run.baseline && run.submitting && !run.saving) {
      // Native submit authors the ID. Bind its unique new user from the owned
      // live projection before any history load; loaded history cannot attest
      // what this submit authored. Raw prefix parity is checked separately.
      const humans = state.messages.filter(
        (message) =>
          message.role === 'user' && !run.previousHumanIds?.has(message.id)
      );
      const human = humans.length === 1 ? humans[0] : undefined;
      if (
        human?.content === run.text &&
        human.delivery.generation &&
        !run.previousGenerations?.has(human.delivery.generation)
      )
        run.turn = captureTurn(run.baseline, {
          owner: selected.identity,
          generation: run.generation,
          threadId: selected.id,
          humanId: human.id,
          humanContent: run.text,
        });
    }
    const plan = planState(state.values),
      rows = selected.content.project(state);
    if (!current(run)) return;
    publish({
      rows,
      messages: state.messages,
      toolCalls: state.toolCalls,
      ...(plan.kind === 'valid' ? { observedPlan: plan, notice: null } : {}),
      ...(plan.kind === 'invalid'
        ? {
            notice:
              'This plan update could not be displayed. Plans support up to 50 items, 2000 characters per item, and pending, in progress, or completed status. The prior valid plan is retained.',
          }
        : {}),
    });
    return current(run) ? state : undefined;
  }
  function finish(
    run: Attempt,
    phase: PlanningApplicationSnapshot['phase'],
    outcome: CompleteOutcome | null,
    error: string | null,
    canSubmit = true
  ) {
    if (!current(run)) return;
    attempt = undefined;
    publish({ busy: false, phase, outcome, error, canSubmit });
  }
  async function submit(text: string) {
    if (disposed || snapshot.busy || !snapshot.canSubmit || !text.trim())
      return false;
    const run: Attempt = {
      controller: new AbortController(),
      generation: crypto.randomUUID(),
      text,
      view: snapshot.viewGeneration,
    };
    attempt = run;
    publish({
      busy: true,
      canSubmit: false,
      phase: 'working',
      outcome: null,
      error: null,
    });
    let nativeSucceeded = false;
    try {
      if (!current(run)) return false;
      if (!owner) {
        const id = await client.createThread(run.controller.signal);
        if (!current(run)) return false;
        if (!id || typeof id !== 'string') throw Error('Unconfirmed creation');
        const selected: Owner = {
          id,
          identity: crypto.randomUUID(),
          session: client.sessionFactory(id),
          content: createMessageContent(),
          closed: false,
          attempted: false,
          checkpointKnown: false,
        };
        if (!current(run)) {
          void close(selected);
          return false;
        }
        owner = selected;
        run.owner = selected;
        selected.release = selected.session.subscribe(() => {
          const active = attempt;
          if (active?.owner === selected && current(active)) {
            try {
              observe(active);
            } catch {
              /* Terminal confirmation will fail closed. */
            }
          }
        });
        if (selected.closed) selected.release();
        if (!current(run)) return false;
        publish({ threadId: id });
      } else run.owner = owner;
      const selected = run.owner;
      if (!selected || !current(run)) return false;
      // Read only after authored Send. An uncertain prior turn may advance the
      // exact canonical prefix without advancing separately retained authority.
      const prior = selected.attempted
        ? await client.readCurrent(selected.id, run.controller.signal)
        : undefined;
      if (!current(run)) return false;
      const values =
        prior === undefined
          ? undefined
          : plainRecord(prior)
          ? prior.values
          : undefined;
      if (prior !== undefined && !plainRecord(values))
        throw Error('Unavailable prefix');
      if (
        plainRecord(prior) &&
        prior.checkpoint !== undefined &&
        !checkpointSource(prior.checkpoint, selected.id)
      )
        throw Error('Foreign current state');
      if (plainRecord(prior) && checkpointSource(prior.checkpoint, selected.id))
        selected.checkpointKnown = true;
      run.baseline = captureBaseline(values, snapshot.savedPlan);
      if (!run.baseline) throw Error('Unavailable prefix');
      selected.attempted = true;
      const before = selected.session.getSnapshot();
      if (!current(run)) return false;
      run.previousHumanIds = new Set(
        before.messages
          .filter((message) => message.role === 'user')
          .map((message) => message.id)
      );
      run.previousGenerations = new Set(
        before.messages.map((message) => message.delivery.generation)
      );
      run.submitting = true;
      const outcome = await selected.session.submit(text, {
        signal: run.controller.signal,
      });
      if (!current(run)) return false;
      const live = observe(run);
      if (!current(run)) return false;
      if (outcome !== 'success') {
        finish(
          run,
          outcome === 'aborted'
            ? 'stopped'
            : outcome === 'error'
            ? 'failed'
            : 'unconfirmed',
          outcome,
          outcome === 'error'
            ? selected.checkpointKnown
              ? 'The response failed. The last saved plan is retained.'
              : 'The response failed before a saved conversation could be confirmed. Start a new conversation to continue.'
            : 'The response was not confirmed. The last saved plan is retained.',
          outcome === 'error' && selected.checkpointKnown
        );
        return false;
      }
      nativeSucceeded = true;
      if (!run.turn) throw Error('Native human was not observed');
      run.saving = true;
      publish({ phase: 'confirming' });
      if (!current(run)) return false;
      await selected.session.load({ signal: run.controller.signal });
      if (!current(run)) return false;
      const saved = observe(run),
        checkpoint =
          saved &&
          checkpointSource(
            saved.history?.[0] && plainRecord(saved.history[0])
              ? saved.history[0].checkpoint
              : undefined,
            selected.id
          );
      if (!saved || !live || !checkpoint) throw Error('Unavailable checkpoint');
      selected.checkpointKnown = true;
      const raw = await client.readCheckpoint(
        checkpoint,
        run.controller.signal
      );
      if (!current(run)) return false;
      const latest = observe(run);
      if (!current(run) || !latest) return false;
      // Raw observedValues and loadedCheckpoint deliberately share this exact
      // owned SDK read. Independent corroboration is native success, current
      // human/prefix, canonical messages/tools, native values and history.
      const confirmation = captureTerminal({
        turn: run.turn,
        owner: selected.identity,
        generation: run.generation,
        threadId: selected.id,
        submittedSuccessfully: outcome === 'success',
        observedCheckpoint: checkpoint,
        observedValues: plainRecord(raw) ? raw.values : undefined,
        loadedCheckpoint: raw,
        snapshot: latest,
      });
      if (!current(run)) return false;
      if (confirmation.kind !== 'confirmed')
        throw Error('Unconfirmed saved plan');
      publish({
        savedPlan: confirmation.plan,
        observedPlan: confirmation.plan,
      });
      finish(run, 'saved', outcome, null);
      return true;
    } catch {
      if (current(run))
        finish(
          run,
          nativeSucceeded ? 'unconfirmed' : 'failed',
          nativeSucceeded ? 'success' : 'error',
          nativeSucceeded
            ? 'The saved plan could not be confirmed. The last saved plan is retained.'
            : run.owner?.attempted && !run.owner.checkpointKnown
            ? 'The request failed before a saved conversation could be confirmed. Start a new conversation to continue.'
            : 'The request failed. You can try again.',
          nativeSucceeded || !run.owner?.attempted || run.owner.checkpointKnown
        );
      return false;
    }
  }
  async function stop() {
    const run = attempt;
    if (disposed || !run) return;
    attempt = undefined;
    run.controller.abort();
    publish({
      busy: false,
      canSubmit: false,
      phase: 'stopped',
      outcome: 'aborted',
      error:
        'Response stopped. The last saved plan is retained. Start a new conversation to continue.',
    });
    try {
      await run.owner?.session.stop();
    } catch {
      /* Cancellation is not save authority. */
    }
  }
  async function newConversation() {
    if (disposed) return;
    attempt?.controller.abort();
    attempt = undefined;
    const previous = owner;
    owner = undefined;
    publish({ ...empty(), viewGeneration: snapshot.viewGeneration + 1 });
    if (previous) await close(previous);
  }
  function dispose() {
    if (disposal) return disposal;
    disposed = true;
    attempt?.controller.abort();
    attempt = undefined;
    const previous = owner;
    owner = undefined;
    if (previous) void close(previous);
    listeners.clear();
    return (disposal = Promise.all([...cleanups]).then(() => undefined));
  }
  return {
    getSnapshot: () => snapshot,
    subscribe(notify: () => void) {
      if (disposed) return () => undefined;
      listeners.add(notify);
      return () => {
        listeners.delete(notify);
      };
    },
    submit,
    stop,
    newConversation,
    dispose,
  };
}
tool-evidence.ts
import { copyJson, plainRecord, planState, type PlanItem } from './plan-state';
 
export type WriteEvidence =
  | { readonly kind: 'none' | 'rejected' | 'invalid' }
  | {
      readonly kind: 'replacement';
      readonly items: readonly PlanItem[];
      readonly callId: string;
    };
 
/** Inspect actual canonical wire messages. Core ToolCall.complete cannot
 * establish success: history projection also completes raw error results. */
export function writeEvidence(input: unknown, humanId: string): WriteEvidence {
  const invalid: WriteEvidence = Object.freeze({ kind: 'invalid' });
  try {
    const messages = copyJson(input);
    if (!Array.isArray(messages) || !humanId) return invalid;
    const messageIds = new Set<string>();
    for (const message of messages) {
      if (
        !plainRecord(message) ||
        typeof message.id !== 'string' ||
        !message.id ||
        messageIds.has(message.id)
      )
        return invalid;
      messageIds.add(message.id);
    }
    const start = messages.findIndex(
      (m) => m.id === humanId && m.type === 'human'
    );
    if (start < 0) return invalid;
    const pending = new Map<
      string,
      { name: string; args: unknown; parallel: boolean }
    >();
    // Prior IDs cannot be reused in the current turn.
    const seen = new Set<string>();
    for (const message of messages.slice(0, start)) {
      if (Array.isArray(message.tool_calls))
        for (const call of message.tool_calls) {
          if (
            !plainRecord(call) ||
            typeof call.id !== 'string' ||
            seen.has(call.id)
          )
            return invalid;
          seen.add(call.id);
        }
    }
    let result: WriteEvidence = Object.freeze({ kind: 'none' });
    for (const [index, message] of messages.slice(start).entries()) {
      if (
        typeof message.content !== 'string' ||
        !empty(message.invalid_tool_calls)
      )
        return invalid;
      const extra = message.additional_kwargs;
      if (
        extra !== undefined &&
        (!plainRecord(extra) ||
          extra.function_call !== undefined ||
          !empty(extra.tool_calls))
      )
        return invalid;
      if (message.type === 'human') {
        if (
          index !== 0 ||
          pending.size ||
          !empty(message.tool_calls) ||
          message.tool_call_id !== undefined
        )
          return invalid;
      } else if (message.type === 'ai') {
        if (pending.size || message.tool_call_id !== undefined) return invalid;
        const calls = message.tool_calls ?? [];
        if (!Array.isArray(calls)) return invalid;
        const parallel =
          calls.filter(
            (call) => plainRecord(call) && call.name === 'write_todos'
          ).length > 1;
        for (const call of calls) {
          if (
            !plainRecord(call) ||
            typeof call.id !== 'string' ||
            !call.id ||
            seen.has(call.id) ||
            typeof call.name !== 'string' ||
            !call.name ||
            !plainRecord(call.args) ||
            (call.type !== undefined && call.type !== 'tool_call')
          )
            return invalid;
          pending.set(call.id, { name: call.name, args: call.args, parallel });
          seen.add(call.id);
        }
      } else if (message.type === 'tool') {
        if (
          typeof message.tool_call_id !== 'string' ||
          !empty(message.tool_calls)
        )
          return invalid;
        const call = pending.get(message.tool_call_id);
        if (!call || !['success', 'error'].includes(message.status as string))
          return invalid;
        if (
          message.name !== call.name &&
          !(
            message.status === 'error' &&
            (message.name === null || message.name === undefined)
          )
        )
          return invalid;
        pending.delete(message.tool_call_id);
        if (call.name === 'write_todos') {
          if (message.status === 'error')
            result = Object.freeze({ kind: 'rejected' });
          else {
            const plan = planState(call.args);
            if (
              call.parallel ||
              plan.kind !== 'valid' ||
              Object.keys(call.args as object).length !== 1
            )
              return invalid;
            result = Object.freeze({
              kind: 'replacement',
              items: plan.items,
              callId: message.tool_call_id,
            });
          }
        }
      } else return invalid;
    }
    return pending.size ? invalid : result;
  } catch {
    return invalid;
  }
}
 
function empty(value: unknown) {
  return value === undefined || (Array.isArray(value) && value.length === 0);
}
terminal.ts
import type { Message, ToolCall } from '@threadplane/core';
import {
  copyJson,
  plainRecord,
  planState,
  sameJson,
  type PlanState,
} from './plan-state';
import { writeEvidence } from './tool-evidence';
 
export interface Baseline {
  readonly messages: readonly unknown[];
  readonly plan: Exclude<PlanState, { readonly kind: 'invalid' }>;
}
export interface TurnIdentity {
  readonly owner: string;
  /** Local application attempt identity, never a backend run id. */
  readonly generation: string;
  readonly threadId: string;
  readonly humanId: string;
  readonly humanContent: string;
  readonly backendRunId?: string;
}
export interface Turn extends TurnIdentity {
  readonly baseline: Baseline;
}
export interface Checkpoint {
  readonly thread_id: string;
  readonly checkpoint_ns: '';
  readonly checkpoint_id: string;
  readonly checkpoint_map?: Readonly<Record<string, string>>;
}
export interface PlanningSnapshot {
  readonly status: string;
  readonly error?: unknown;
  readonly interrupts: readonly unknown[];
  readonly subgraphs: readonly unknown[];
  readonly messages: readonly Message[];
  readonly toolCalls: readonly ToolCall[];
  readonly values?: unknown;
  readonly history?: readonly unknown[];
}
export interface TerminalInput {
  readonly turn: Turn | undefined;
  readonly owner: string;
  readonly generation: string;
  readonly threadId: string;
  readonly backendRunId?: string;
  /** Only true for the native submit result outcome === 'success'. A resolved
   * promise alone also covers failed, paused and cancelled outcomes. */
  readonly submittedSuccessfully: boolean;
  /** Root checkpoint identity and values actually observed for this attempt. */
  readonly observedCheckpoint: unknown;
  readonly observedValues: unknown;
  /** Result of loading that exact root checkpoint, never a current-state guess. */
  readonly loadedCheckpoint: unknown;
  readonly snapshot: PlanningSnapshot;
}
export type Confirmation =
  | { readonly kind: 'unconfirmed' }
  | {
      readonly kind: 'confirmed';
      readonly update: 'replacement' | 'retained';
      readonly plan: Baseline['plan'];
      readonly checkpoint: Checkpoint;
      readonly values: Readonly<Record<string, unknown>>;
    };
 
/** Capture the exact current pre-send canonical prefix. Undefined is the inert
 * first turn. Default plan authority is safe only for initial/already-confirmed
 * values. After an unconfirmed attempt, supply the separately retained saved
 * plan: current canonical messages may advance without advancing authority.
 * Current malformed todos do not prevent a later successful replacement. */
export function captureBaseline(
  input: unknown,
  retainedSavedPlan?: Baseline['plan']
): Baseline | undefined {
  try {
    const values = copyJson(input === undefined ? { messages: [] } : input);
    if (!plainRecord(values) || !Array.isArray(values.messages))
      return undefined;
    const plan =
      retainedSavedPlan === undefined
        ? planState(values)
        : savedPlanSource(retainedSavedPlan);
    if (!plan || plan.kind === 'invalid') return undefined;
    return Object.freeze({ messages: values.messages, plan });
  } catch {
    return undefined;
  }
}
 
function savedPlanSource(input: unknown): Baseline['plan'] | undefined {
  const value = copyJson(input);
  if (!plainRecord(value)) return undefined;
  if (value.kind === 'missing' && Object.keys(value).length === 1)
    return Object.freeze({ kind: 'missing' });
  if (
    value.kind !== 'valid' ||
    Object.keys(value).length !== 2 ||
    !Object.hasOwn(value, 'items')
  )
    return undefined;
  const plan = planState({ todos: value.items });
  return plan.kind === 'valid' ? plan : undefined;
}
 
/** Bind the captured baseline only after the native runtime supplies its actual
 * current human id. These are application facts, not invented graph fields. */
export function captureTurn(
  baseline: Baseline | undefined,
  input: TurnIdentity
): Turn | undefined {
  try {
    const identity = copyJson(input, true);
    if (
      !baseline ||
      !plainRecord(identity) ||
      ['owner', 'generation', 'threadId', 'humanId'].some(
        (k) => typeof identity[k] !== 'string' || !identity[k]
      ) ||
      typeof identity.humanContent !== 'string' ||
      (identity.backendRunId !== undefined &&
        (typeof identity.backendRunId !== 'string' || !identity.backendRunId))
    )
      return undefined;
    const copied = copyJson(baseline);
    if (
      !plainRecord(copied) ||
      !Array.isArray(copied.messages) ||
      !savedPlanSource(copied.plan) ||
      copied.messages.some((m) => plainRecord(m) && m.id === identity.humanId)
    )
      return undefined;
    return Object.freeze({ ...identity, baseline: copied }) as unknown as Turn;
  } catch {
    return undefined;
  }
}
 
export function checkpointSource(
  input: unknown,
  threadId: string
): Checkpoint | undefined {
  try {
    const value = copyJson(input);
    if (
      !plainRecord(value) ||
      value.thread_id !== threadId ||
      value.checkpoint_ns !== '' ||
      typeof value.checkpoint_id !== 'string' ||
      !value.checkpoint_id ||
      Object.keys(value).some(
        (k) =>
          ![
            'thread_id',
            'checkpoint_ns',
            'checkpoint_id',
            'checkpoint_map',
          ].includes(k)
      )
    )
      return undefined;
    if (
      value.checkpoint_map !== undefined &&
      (!plainRecord(value.checkpoint_map) ||
        Object.entries(value.checkpoint_map).some(
          ([k, v]) => k !== '' || v !== value.checkpoint_id
        ))
    )
      return undefined;
    return value as unknown as Checkpoint;
  } catch {
    return undefined;
  }
}
function empty(value: unknown) {
  return value === undefined || (Array.isArray(value) && !value.length);
}
function clean(record: Record<string, unknown>) {
  return (
    (record.error === undefined || record.error === null) &&
    ['interrupts', '__interrupt__', 'subgraphs'].every((k) => empty(record[k]))
  );
}
function terminal(record: Record<string, unknown>) {
  return (
    clean(record) &&
    Array.isArray(record.next) &&
    !record.next.length &&
    Array.isArray(record.tasks) &&
    !record.tasks.length
  );
}
function projectValues(values: Record<string, unknown>) {
  return Object.fromEntries(
    Object.entries(values).filter(
      ([k]) => k !== 'messages' && k !== '__interrupt__'
    )
  );
}
 
/** Confirmation is fail-closed. An unconfirmed result carries no replacement;
 * the application retains its previous saved authority until a later success. */
export function captureTerminal(input: TerminalInput): Confirmation {
  const unconfirmed: Confirmation = Object.freeze({ kind: 'unconfirmed' });
  try {
    const turn = copyJson(input.turn, true);
    const state = copyJson(input.snapshot, true);
    const raw = copyJson(input.loadedCheckpoint);
    const observed = copyJson(input.observedValues);
    if (
      !plainRecord(turn) ||
      !plainRecord(state) ||
      !plainRecord(raw) ||
      !plainRecord(observed) ||
      input.submittedSuccessfully !== true ||
      turn.owner !== input.owner ||
      turn.generation !== input.generation ||
      turn.threadId !== input.threadId ||
      state.status !== 'idle' ||
      !clean(state) ||
      !Array.isArray(state.interrupts) ||
      state.interrupts.length ||
      !Array.isArray(state.subgraphs) ||
      state.subgraphs.length ||
      !terminal(raw)
    )
      return unconfirmed;
    const knownRunId = input.backendRunId ?? turn.backendRunId;
    if (
      turn.backendRunId !== undefined &&
      input.backendRunId !== undefined &&
      turn.backendRunId !== input.backendRunId
    )
      return unconfirmed;
    if (
      knownRunId !== undefined &&
      (typeof knownRunId !== 'string' ||
        !knownRunId ||
        (raw.run_id !== undefined && raw.run_id !== knownRunId) ||
        (plainRecord(raw.metadata) &&
          raw.metadata.run_id !== undefined &&
          raw.metadata.run_id !== knownRunId))
    )
      return unconfirmed;
    const requested = checkpointSource(
      input.observedCheckpoint,
      input.threadId
    );
    const checkpoint = checkpointSource(raw.checkpoint, input.threadId);
    const latest = Array.isArray(state.history) ? state.history[0] : undefined;
    // Native history deliberately carries only checkpoint/parent/date/next.
    // Raw saved execution tasks and values are proved by the exact state read.
    if (
      !requested ||
      !checkpoint ||
      !sameJson(requested, checkpoint) ||
      !plainRecord(latest) ||
      !clean(latest) ||
      !Array.isArray(latest.next) ||
      latest.next.length ||
      (latest.tasks !== undefined &&
        (!Array.isArray(latest.tasks) || latest.tasks.length)) ||
      !sameJson(
        checkpointSource(latest.checkpoint, input.threadId),
        checkpoint
      ) ||
      (latest.values !== undefined && !sameJson(latest.values, raw.values))
    )
      return unconfirmed;
    const values = raw.values;
    if (
      !plainRecord(values) ||
      !clean(values) ||
      !sameJson(values, observed) ||
      !sameJson(projectValues(values), state.values) ||
      !Array.isArray(values.messages) ||
      !Array.isArray(state.messages) ||
      !Array.isArray(state.toolCalls) ||
      !plainRecord(turn.baseline) ||
      !Array.isArray(turn.baseline.messages)
    )
      return unconfirmed;
    const prefix = turn.baseline.messages;
    if (!sameJson(prefix, values.messages.slice(0, prefix.length)))
      return unconfirmed;
    const human = values.messages[prefix.length];
    if (
      !plainRecord(human) ||
      human.type !== 'human' ||
      human.id !== turn.humanId ||
      human.content !== turn.humanContent
    )
      return unconfirmed;
    const evidence = writeEvidence(values.messages, turn.humanId as string);
    if (evidence.kind === 'invalid' || evidence.kind === 'rejected')
      return unconfirmed;
    const plan = planState(values);
    if (
      plan.kind === 'invalid' ||
      (evidence.kind === 'replacement'
        ? plan.kind !== 'valid' || !sameJson(plan.items, evidence.items)
        : !sameJson(plan, turn.baseline.plan))
    )
      return unconfirmed;
    if (!canonicalMatches(values.messages, state.messages, state.toolCalls))
      return unconfirmed;
    const last = values.messages.at(-1);
    if (
      !plainRecord(last) ||
      last.type !== 'ai' ||
      typeof last.content !== 'string' ||
      !last.content.trim() ||
      !empty(last.tool_calls)
    )
      return unconfirmed;
    return Object.freeze({
      kind: 'confirmed',
      update: evidence.kind === 'replacement' ? 'replacement' : 'retained',
      plan,
      checkpoint,
      values,
    });
  } catch {
    return unconfirmed;
  }
}
 
function canonicalMatches(
  raw: unknown[],
  messages: unknown[],
  tools: unknown[]
): boolean {
  if (raw.length !== messages.length) return false;
  const calls = new Map<string, Record<string, unknown>>();
  const ids = new Set<string>();
  for (const tool of tools) {
    if (
      !plainRecord(tool) ||
      typeof tool.id !== 'string' ||
      calls.has(tool.id) ||
      tool.status !== 'complete'
    )
      return false;
    calls.set(tool.id, tool);
  }
  const pending = new Set<string>();
  const seen = new Set<string>();
  for (const [i, wire] of raw.entries()) {
    const m = messages[i];
    if (
      !plainRecord(wire) ||
      !clean(wire) ||
      !plainRecord(m) ||
      typeof wire.id !== 'string' ||
      !wire.id ||
      ids.has(wire.id) ||
      wire.id !== m.id ||
      typeof wire.content !== 'string' ||
      wire.content !== m.content ||
      !plainRecord(m.delivery) ||
      m.delivery.phase !== 'complete' ||
      m.delivery.outcome !== 'success' ||
      wire.type !==
        (m.role === 'user'
          ? 'human'
          : m.role === 'assistant'
          ? 'ai'
          : m.role === 'tool'
          ? 'tool'
          : '') ||
      m.name !== (typeof wire.name === 'string' ? wire.name : undefined) ||
      !empty(wire.invalid_tool_calls) ||
      !empty(m.citations)
    )
      return false;
    ids.add(wire.id);
    const extra = plainRecord(wire.additional_kwargs)
      ? wire.additional_kwargs
      : {};
    if (
      (wire.additional_kwargs !== undefined &&
        !plainRecord(wire.additional_kwargs)) ||
      extra.function_call !== undefined ||
      !empty(extra.tool_calls) ||
      !empty(extra.citations) ||
      !empty(extra.sources)
    )
      return false;
    const reasoning =
      typeof wire.reasoning === 'string'
        ? wire.reasoning
        : typeof extra.reasoning_content === 'string'
        ? extra.reasoning_content
        : undefined;
    if (m.reasoning !== reasoning || m.toolCallId !== wire.tool_call_id)
      return false;
    if (wire.type === 'ai') {
      if (pending.size) return false;
      const rawCalls = wire.tool_calls ?? [];
      const projectedIds = m.toolCallIds ?? [];
      if (
        !Array.isArray(rawCalls) ||
        !Array.isArray(projectedIds) ||
        rawCalls.length !== projectedIds.length
      )
        return false;
      for (const [j, call] of rawCalls.entries()) {
        if (
          !plainRecord(call) ||
          typeof call.id !== 'string' ||
          !call.id ||
          call.id !== projectedIds[j] ||
          seen.has(call.id)
        )
          return false;
        const tool = calls.get(call.id);
        if (!tool || tool.name !== call.name || !sameJson(tool.args, call.args))
          return false;
        seen.add(call.id);
        pending.add(call.id);
      }
    } else if (wire.type === 'tool') {
      const tool = calls.get(wire.tool_call_id as string);
      if (
        !tool ||
        !pending.delete(wire.tool_call_id as string) ||
        tool.result !== wire.content ||
        !['success', 'error'].includes(wire.status as string) ||
        (wire.name !== tool.name &&
          !(
            wire.status === 'error' &&
            (wire.name === null || wire.name === undefined)
          )) ||
        !empty(wire.tool_calls) ||
        !empty(m.toolCallIds)
      )
        return false;
    } else if (
      pending.size ||
      !empty(wire.tool_calls) ||
      !empty(m.toolCallIds) ||
      wire.tool_call_id !== undefined
    )
      return false;
  }
  return !pending.size && seen.size === calls.size;
}

Stop and recover

Stop keeps the last saved plan visible and requires New conversation before another Send. A first native failure before any saved checkpoint also requires New conversation. A later unconfirmed request keeps the saved plan separate; a later valid write can recover against the current conversation prefix. A response with no write can retain an unchanged, previously confirmed plan, but cannot adopt changed, unconfirmed todos. These are local display and submission rules, not a claim that stopping rolls back server state. There is no automatic replay or resume.

Build from source

From a Threadplane checkout with dependencies installed:

npx nx build cockpit-deep-agents-planning-react
npx nx e2e cockpit-deep-agents-planning-react

Browser verification uses the real SDK, compiled Python graph and saved checkpoints with deterministic providers. Verification endpoints are excluded from public output.

Looking for something specific?