Page actions

React Deep Agents Filesystem preview

Show the files an agent is working with and ask for a decision before protected changes. A workspace view and an approval control serve different purposes even when they sit beside the same chat.

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.

Separate observation from permission

The application reads the graph’s workspace projection for display and independently matches the pending protected-action batch before enabling a decision. A file appearing in the panel does not authorize an edit. The source below separates workspace parsing, decision policy, and native React presentation so each can be adapted without merging their responsibilities.

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

Start a runway note

Choose Run, type a message and choose Send or press Enter. Runway note for KASE fills this exact draft without sending:

Work up a runway suitability note for KASE. Save your raw lookups to /notes/kase-data.md, then write the finished note to /reports/kase-runway.md.

Airport elevation and runway lookups use fixed tables in the unchanged graph. Only Send creates the owned thread. New conversation clears local files, selection, approval and conversation ownership; it waits for Send to create another thread. Opening the guide, changing frontend or mode, using browser history, filling a suggestion and choosing New do not author runs. Mode navigation and 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 FilesystemConnection,
} from './connection';
import type { FilesystemApplicationSnapshot } from './application';
import { WorkspacePanel } from './workspace-panel';
import { ApprovalPanel } from './approval-panel';
const literal = (value: unknown) =>
  typeof value === 'string' ? value : JSON.stringify(value, null, 2);
export function renderMessage(
  row: MessageRow,
  snapshot: FilesystemApplicationSnapshot
) {
  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; workspace confirmation is shown separately.'
                  : `Observed tool: ${call.status}`
              }
            />
          </details>
        ))}
    </article>
  );
}
export function connectionFingerprint(connection: FilesystemConnection) {
  return JSON.stringify([
    connection.apiUrl,
    Object.entries(connection.headers).sort(([a], [b]) => a.localeCompare(b)),
  ]);
}
type Props = {
  readonly connection: FilesystemConnection;
  readonly onReady: () => void;
};
export function FilesystemDemo(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 <FilesystemOwner key={identity.current.version} {...props} />;
}
function FilesystemOwner({ 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 workspace…'
      : 'Receiving response…'
    : snapshot.phase === 'paused'
    ? 'Awaiting approval.'
    : snapshot.phase === 'saved'
    ? 'Response saved.'
    : snapshot.phase === 'stopped'
    ? 'Response stopped.'
    : snapshot.phase === 'unconfirmed'
    ? 'Workspace update unconfirmed.'
    : snapshot.phase === 'failed'
    ? 'Response failed.'
    : 'Ready.';
  return (
    <ExampleLayout className="filesystem-demo">
      <header>
        <div>
          <h1>A workspace 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 runway note using fixed aviation lookup data. Read actual files
        and review proposed report changes before they run.
      </p>
      <div className="filesystem-layout">
        <section className="conversation-panel" aria-label="Chat">
          <div className="seed-prompts" aria-label="Example prompts">
            {[
              {
                label: 'Runway note for KASE',
                text: 'Work up a runway suitability note for KASE. Save your raw lookups to /notes/kase-data.md, then write the finished note to /reports/kase-runway.md.',
              },
            ].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 runway note…"
              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>
        <div className="workspace-column">
          <WorkspacePanel
            key={`workspace:${snapshot.viewGeneration}`}
            observed={snapshot.observedWorkspace}
            saved={snapshot.savedWorkspace}
            phase={snapshot.phase}
            notice={snapshot.notice}
          />
          <ApprovalPanel
            approval={snapshot.approval}
            workspace={snapshot.savedWorkspace}
            busy={snapshot.busy}
            onDecision={(choice) => {
              const current = application.getSnapshot();
              if (
                currentView() &&
                !current.busy &&
                current.phase === 'paused' &&
                current.approval === snapshot.approval
              )
                void application.decide(choice, snapshot.decisionToken);
            }}
          />
        </div>
      </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 {
  createFilesystemApplication,
  type FilesystemClient,
  type FilesystemSession,
} from './application';
import { copyJson } from './workspace-state';
import { checkpointSource, type Checkpoint } from './authority';
export interface FilesystemConnection {
  readonly apiUrl: string;
  readonly headers: Record<string, string>;
}
export function filesystemConnection(
  configuration: RuntimeBridgeConfiguration
): FilesystemConnection {
  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 createFilesystemClient(
  input: FilesystemConnection
): FilesystemClient {
  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-filesystem',
          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) ||
          state.messages.length !== 0 ||
          state.toolCalls.length !== 0 ||
          state.interrupts.length !== 0 ||
          state.subgraphs.length !== 0 ||
          (state.history !== undefined &&
            (!Array.isArray(state.history) || state.history.length !== 0)) ||
          ![
            'getSnapshot',
            'subscribe',
            'submit',
            'resume',
            'load',
            'stop',
            'dispose',
          ].every(
            (method) =>
              typeof session?.[method as keyof typeof session] === 'function'
          )
        )
          throw Error('Rejected session');
        return session as FilesystemSession;
      } 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: FilesystemConnection) {
  return createFilesystemApplication(createFilesystemClient(connection));
}

Read the actual workspace

The workspace groups paths by directory in stable path order. Select a file to read its literal text. An explicit selection survives while its path exists; otherwise the panel selects the first existing path deterministically. Current UTF-8 records use their content string; valid legacy arrays of string lines join with newline. Empty text stays empty. Files are read-only and are never interpreted as HTML or Markdown.

The panel distinguishes Live workspace from checkpoint-confirmed files labeled Paused · confirmed workspace, Saved workspace or Last confirmed workspace. Streamed values can describe a change before checkpoint confirmation. At a confirmed pause, the saved panel shows actual files from the current root checkpoint beside proposed actions. A proposed file is awaiting approval; it is not a saved file.

The projection accepts at most 100 canonical paths of at most 1024 UTF-16 code units, with at most 65,536 code units per file and 1,048,576 total text units. Unsupported encodings, malformed records and oversized text remain visibly unavailable. Invalid workspace data retains the last confirmed workspace with a notice. The preview does not silently truncate content or label unsupported files Saved.

workspace-state.ts
export const MAX_TEXT = 65536;
export const MAX_TOTAL_TEXT = 1048576;
export type PlainValue =
  | null
  | string
  | boolean
  | number
  | readonly PlainValue[]
  | { readonly [key: string]: PlainValue };
export function plainRecord(value: unknown): value is Record<string, unknown> {
  return (
    value !== null &&
    typeof value === 'object' &&
    Object.getPrototypeOf(value) === Object.prototype
  );
}
export function ownValue(object: object, key: string): unknown {
  const descriptor = Object.getOwnPropertyDescriptor(object, key);
  if (!descriptor) return undefined;
  if (!('value' in descriptor) || (key !== 'length' && !descriptor.enumerable))
    throw new Error('Non-data property');
  return descriptor.value;
}
/** Inspect descriptors before reading. Never evaluate getters or drop keys. */
export function inspectJson(
  input: unknown,
  optionalUndefined = false,
  filesystemValues: readonly (readonly string[])[] = []
): void {
  const parents = new Set<object>();
  let nodes = 0,
    fileLines = 0;
  // Only explicitly authorized values.files[path].content arrays receive this
  // separate descriptor traversal budget. Generic JSON evidence stays at 100k
  // nodes. A supported file has at most MAX_TEXT + 1 legacy lines; one extra
  // line is inspectable as a visibly unavailable oversized record.
  const maxRawLines = MAX_TEXT + 2;
  const maxFileLines = filesystemValues.length * 100 * maxRawLines;
  function visit(value: unknown, depth: number, path: readonly string[]): void {
    if (++nodes > 100000 || depth > 100)
      throw new Error('JSON bounds exceeded');
    if (
      value === null ||
      typeof value === 'string' ||
      typeof value === 'boolean' ||
      (typeof value === 'number' && Number.isFinite(value))
    )
      return;
    const array = Array.isArray(value);
    if (!array && !plainRecord(value)) throw new Error('Unsupported JSON');
    const object = value as object;
    if (
      parents.has(object) ||
      (array && Object.getPrototypeOf(value) !== Array.prototype)
    )
      throw new Error('Unsupported JSON');
    parents.add(object);
    const keys = Reflect.ownKeys(object).filter(
      (k) => !(array && k === 'length')
    );
    const length = array ? (ownValue(object, 'length') as number) : 0;
    if (array && keys.length !== length) throw new Error('Sparse array');
    const fileContent =
      array &&
      filesystemValues.some(
        (root) =>
          path.length === root.length + 3 &&
          root.every((part, i) => path[i] === part) &&
          path[root.length] === 'files' &&
          canonicalPath(path[root.length + 1]) &&
          path[root.length + 2] === 'content'
      );
    if (fileContent && length > maxRawLines)
      throw new Error('Raw legacy file line bounds exceeded');
    for (const key of keys) {
      const d = Object.getOwnPropertyDescriptor(object, key);
      if (
        typeof key !== 'string' ||
        !d ||
        !('value' in d) ||
        !d.enumerable ||
        (array && (!/^(0|[1-9]\d*)$/.test(key) || Number(key) >= length))
      )
        throw new Error('Non-JSON property');
      if (!array && optionalUndefined && d.value === undefined) continue;
      if (fileContent && typeof d.value === 'string') {
        if (++fileLines > maxFileLines)
          throw new Error('Raw legacy workspace line bounds exceeded');
      } else visit(d.value, depth + 1, [...path, key]);
    }
    parents.delete(object);
  }
  visit(input, 0, []);
}
export function copyJson(
  input: unknown,
  optionalUndefined = false,
  maxText = 8388608
): unknown {
  const parents = new Set<object>();
  let nodes = 0,
    text = 0;
  function copy(value: unknown, depth: number): unknown {
    if (++nodes > 100000 || depth > 100)
      throw new Error('JSON bounds exceeded');
    if (typeof value === 'string') {
      text += value.length;
      if (text > maxText) throw new Error('Evidence text bounds exceeded');
      return value;
    }
    if (
      value === null ||
      typeof value === 'boolean' ||
      (typeof value === 'number' && Number.isFinite(value))
    )
      return value;
    const array = Array.isArray(value);
    if (
      (!array && !plainRecord(value)) ||
      (array && Object.getPrototypeOf(value) !== Array.prototype) ||
      parents.has(value as object)
    )
      throw new Error('Unsupported JSON');
    const object = value as object;
    parents.add(object);
    const descriptors = Object.getOwnPropertyDescriptors(object);
    const keys = Reflect.ownKeys(descriptors).filter(
      (k) => !(array && k === 'length')
    );
    const length = array ? descriptors.length?.value : 0;
    if (
      array &&
      (!Number.isInteger(length) || length < 0 || keys.length !== length)
    )
      throw new Error('Sparse array');
    const result: Record<string, unknown> = {};
    for (const key of keys) {
      if (typeof key !== 'string') throw new Error('Symbol key');
      const d = descriptors[key];
      if (
        !d ||
        !('value' in d) ||
        !d.enumerable ||
        (array && (!/^(0|[1-9]\d*)$/.test(key) || Number(key) >= length))
      )
        throw new Error('Non-JSON property');
      if (!array && optionalUndefined && d.value === undefined) continue;
      text += key.length;
      if (text > maxText) throw new Error('Evidence text bounds exceeded');
      Object.defineProperty(result, key, {
        value: copy(d.value, depth + 1),
        enumerable: true,
      });
    }
    parents.delete(object);
    return Object.freeze(
      array ? Array.from({ length }, (_, i) => result[String(i)]) : result
    );
  }
  return copy(input, 0);
}
export function sameJson(left: unknown, right: unknown): boolean {
  if (left === right) return true;
  if (Array.isArray(left) && Array.isArray(right)) {
    const length = ownValue(left, 'length') as number;
    if (length !== ownValue(right, 'length')) return false;
    for (let i = 0; i < length; i++)
      if (!sameJson(ownValue(left, String(i)), ownValue(right, String(i))))
        return false;
    return true;
  }
  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(ownValue(left, k), ownValue(right, k))
    )
  );
}
export function canonicalPath(path: unknown): path is string {
  return (
    typeof path === 'string' &&
    path.length <= 1024 &&
    path.startsWith('/') &&
    !/[\u0000-\u001f\u007f-\u009f\\]/.test(path) &&
    path
      .slice(1)
      .split('/')
      .every((s) => s !== '' && s !== '.' && s !== '..')
  );
}
export type WorkspaceFile =
  | {
      readonly path: string;
      readonly kind: 'text';
      readonly content: string;
      readonly created_at?: string;
      readonly modified_at?: string;
    }
  | {
      readonly path: string;
      readonly kind: 'unavailable';
      readonly reason: string;
    };
export type WorkspaceState =
  | { readonly kind: 'missing'; readonly files: readonly WorkspaceFile[] }
  | { readonly kind: 'invalid'; readonly reason: string }
  | {
      readonly kind: 'valid';
      readonly files: readonly WorkspaceFile[];
      readonly complete: boolean;
    };
export function workspaceState(input: unknown): WorkspaceState {
  const missing = (): WorkspaceState =>
    Object.freeze({ kind: 'missing', files: Object.freeze([]) });
  if (input === undefined) return missing();
  try {
    // Inspect without copying legacy lines, then enforce budgets before joining.
    inspectJson(input, false, [[]]);
    if (!plainRecord(input)) throw new Error();
    if (!Object.hasOwn(input, 'files')) return missing();
    const map = ownValue(input, 'files');
    if (
      !plainRecord(map) ||
      Object.keys(map).length > 100 ||
      !Object.keys(map).every(canonicalPath)
    )
      throw new Error();
    let total = 0;
    const files = Object.keys(map)
      .sort()
      .map((path): WorkspaceFile => {
        const record = ownValue(map, path);
        const unavailable = (reason: string): WorkspaceFile =>
          Object.freeze({ path, kind: 'unavailable', reason });
        if (
          !plainRecord(record) ||
          Object.keys(record).some(
            (k) =>
              !['content', 'encoding', 'created_at', 'modified_at'].includes(k)
          ) ||
          !Object.hasOwn(record, 'content')
        )
          return unavailable('Unsupported file record');
        const encoding = ownValue(record, 'encoding');
        if (encoding !== undefined && encoding !== 'utf-8')
          return unavailable('Unsupported encoding');
        for (const key of ['created_at', 'modified_at'])
          if (
            ownValue(record, key) !== undefined &&
            (typeof ownValue(record, key) !== 'string' ||
              (ownValue(record, key) as string).length > 1024)
          )
            return unavailable('Unsupported timestamp');
        const raw = ownValue(record, 'content');
        let length: number;
        if (typeof raw === 'string') length = raw.length;
        else if (Array.isArray(raw)) {
          const count = ownValue(raw, 'length') as number;
          if (count > MAX_TEXT + 1)
            return unavailable('File content exceeds display limit');
          length = Math.max(0, count - 1);
          for (let i = 0; i < count; i++) {
            const line = ownValue(raw, String(i));
            if (typeof line !== 'string')
              return unavailable('Unsupported file content');
            length += line.length;
            if (length > MAX_TEXT) break;
          }
        } else return unavailable('Unsupported file content');
        if (length > MAX_TEXT)
          return unavailable('File content exceeds display limit');
        if (total + length > MAX_TOTAL_TEXT)
          return unavailable('Workspace content exceeds display budget');
        total += length;
        return Object.freeze({
          path,
          kind: 'text',
          content:
            typeof raw === 'string'
              ? raw
              : (copyJson(raw) as string[]).join('\n'),
          ...(ownValue(record, 'created_at') !== undefined
            ? { created_at: ownValue(record, 'created_at') as string }
            : {}),
          ...(ownValue(record, 'modified_at') !== undefined
            ? { modified_at: ownValue(record, 'modified_at') as string }
            : {}),
        });
      });
    return Object.freeze({
      kind: 'valid',
      files: Object.freeze(files),
      complete: files.every((f) => f.kind === 'text'),
    });
  } catch {
    return Object.freeze({
      kind: 'invalid',
      reason: 'Malformed or unbounded files data',
    });
  }
}
export function selectWorkspacePath(
  state: WorkspaceState,
  selected?: string
): string | undefined {
  return state.kind === 'valid'
    ? state.files.find((f) => f.path === selected)?.path ?? state.files[0]?.path
    : undefined;
}
workspace-panel.tsx
import { useEffect, useState } from 'react';
import { selectWorkspacePath, type WorkspaceState } from './workspace-state';
import type { FilesystemApplicationSnapshot } from './application';
type Props = {
  observed: WorkspaceState;
  saved: WorkspaceState;
  phase: FilesystemApplicationSnapshot['phase'];
  notice: string | null;
};
export function WorkspacePanel({ observed, saved, phase, notice }: Props) {
  const live = phase === 'working' || phase === 'confirming';
  const state = live ? observed : saved;
  const [selected, setSelected] = useState<string>();
  const path = selectWorkspacePath(state, selected);
  useEffect(() => {
    if (path !== selected) setSelected(path);
  }, [path, selected]);
  const files =
    state.kind === 'valid' || state.kind === 'missing' ? state.files : [];
  const file = files.find((x) => x.path === path);
  const groups = [
    ...new Set(
      files.map((x) => x.path.slice(0, x.path.lastIndexOf('/')) || '/')
    ),
  ];
  return (
    <section className="workspace-panel" aria-label="Workspace">
      <h2>
        {live
          ? 'Live workspace'
          : phase === 'paused'
          ? 'Paused · confirmed workspace'
          : phase === 'saved'
          ? 'Saved workspace'
          : 'Last confirmed workspace'}
      </h2>
      <p>
        {live
          ? 'Observed files while the response runs. Saved files are confirmed separately.'
          : phase === 'paused'
          ? 'These files come from the saved pause checkpoint. Proposed changes below have not run.'
          : 'Read-only file text from the last confirmed checkpoint.'}
      </p>
      {notice && <p role="status">{notice}</p>}
      {state.kind === 'valid' && !files.length && (
        <p>The workspace is empty.</p>
      )}
      {state.kind === 'missing' && (
        <p>No confirmed files map yet. Send a message to begin.</p>
      )}
      {state.kind === 'invalid' && (
        <p role="status">Files unavailable: {state.reason}</p>
      )}
      <div className="file-layout">
        <nav aria-label="Files">
          {groups.map((group) => (
            <div key={group}>
              <h3>{group}</h3>
              {files
                .filter(
                  (x) =>
                    (x.path.slice(0, x.path.lastIndexOf('/')) || '/') === group
                )
                .map((x) => (
                  <button
                    type="button"
                    key={x.path}
                    aria-pressed={x.path === path}
                    onClick={() => setSelected(x.path)}
                  >
                    {x.path}
                  </button>
                ))}
            </div>
          ))}
        </nav>
        {file && (
          <article aria-label="File preview">
            <h3>{file.path}</h3>
            {file.kind === 'text' ? (
              <>
                <pre tabIndex={0} aria-label="Literal file content">
                  {file.content}
                </pre>
                {file.content === '' && <p>Empty file</p>}
              </>
            ) : (
              <p role="status">{file.reason}</p>
            )}
          </article>
        )}
      </div>
    </section>
  );
}

Review the whole protected batch

The unchanged graph uses StateBackend and an interrupt permission over /reports/**. In the pinned backend, write_file and edit_file at the exact path /reports are allowed; report descendants are protected. Deletes covering /reports or the root are protected because their subtree overlaps the rule. Notes may run in parallel with a protected report call: an actual mixed pause can have an empty saved files map and three pending tool calls while requiring just one protected report decision. Count action_requests in the interrupt, not every unresolved tool call.

Every proposed action appears in its original order, including duplicate target paths. A write to an existing file is a replacement proposal: compare the actual saved text in the workspace with proposed new text in the approval panel. A new write is a new file proposal. An edit shows its actual old_string, new_string and replace_all setting; the UI does not predict the edited result. A delete shows the target path and file or subtree intent.

The controls offer Approve entire batch and Reject entire batch, only where the choice is permitted by every review config. There are no Edit or Respond controls. A supported batch contains at most 20 recognized write_file, edit_file or delete actions with bounded canonical arguments. Unknown arguments and malformed proposals leave decisions unavailable with an explanation and New conversation available. A malformed whole workspace map or unconfirmed checkpoint ownership blocks confirmation and decisions. Unsupported individual file records remain visibly unavailable and never supply literal file text; they do not by themselves block a supported batch whose raw saved map is confirmed. The example sends one decisions object containing exactly one ordered approve or reject decision for each protected action request, with no partial decision or automatic approval.

approval-state.ts
import {
  canonicalPath,
  copyJson,
  MAX_TEXT,
  MAX_TOTAL_TEXT,
  plainRecord,
  sameJson,
  type PlainValue,
} from './workspace-state';
export type Choice = 'approve' | 'reject';
// Native HITL embeds the Python repr of complete args in description. A pair
// of maximally escaped text arguments can exceed 8 KiB by a large margin.
export const MAX_DESCRIPTION = 1048576;
export const MAX_METADATA = 2097152;
export interface ApprovalAction {
  readonly name: 'write_file' | 'edit_file' | 'delete';
  readonly args: Readonly<Record<string, PlainValue>>;
  readonly description: string;
}
export type ApprovalState =
  | {
      readonly kind: 'unavailable';
      readonly reason: string;
      readonly rawProposal?: PlainValue;
    }
  | {
      readonly kind: 'valid';
      readonly interruptId: string;
      readonly actions: readonly ApprovalAction[];
      readonly choices: readonly Choice[];
      readonly signature: string;
    };
const exactKeys = (
  r: Record<string, unknown>,
  required: string[],
  optional: string[] = []
) =>
  required.every((k) => Object.hasOwn(r, k)) &&
  Object.keys(r).every((k) => required.includes(k) || optional.includes(k));
/** Pinned WriteFileSchema, EditFileSchema, DeleteSchema; never normalize args. */
export function mutationArgs(name: unknown, args: unknown): boolean {
  if (
    !plainRecord(args) ||
    !(
      canonicalPath(args.file_path) ||
      (name === 'delete' && args.file_path === '/')
    )
  )
    return false;
  const text = (key: string) =>
    typeof args[key] === 'string' && (args[key] as string).length <= MAX_TEXT;
  if (name === 'write_file')
    return exactKeys(args, ['file_path', 'content']) && text('content');
  if (name === 'edit_file')
    return (
      exactKeys(
        args,
        ['file_path', 'old_string', 'new_string'],
        ['replace_all']
      ) &&
      text('old_string') &&
      text('new_string') &&
      (!Object.hasOwn(args, 'replace_all') ||
        typeof args.replace_all === 'boolean')
    );
  return name === 'delete' && exactKeys(args, ['file_path']);
}
export function approvalState(input: unknown): ApprovalState {
  let rawProposal: PlainValue | undefined;
  try {
    input = copyJson(input);
    // Capture unsupported input only if the entire raw JSON is bounded. Never
    // rewrite a path, omit an unknown mutation key, or evaluate an accessor.
    let rawText = 0;
    const count = (v: unknown): void => {
      if (typeof v === 'string') rawText += v.length;
      else if (Array.isArray(v)) v.forEach(count);
      else if (plainRecord(v))
        Object.entries(v).forEach(([k, x]) => {
          rawText += k.length;
          count(x);
        });
      if (rawText > MAX_METADATA + MAX_TOTAL_TEXT)
        throw new Error('Raw proposal exceeds visible budget');
    };
    count(input);
    rawProposal = input as PlainValue;
    if (
      !plainRecord(input) ||
      !exactKeys(input, ['id', 'value']) ||
      typeof input.id !== 'string' ||
      !input.id ||
      input.id.length > 1024 ||
      !plainRecord(input.value) ||
      !exactKeys(input.value, ['action_requests', 'review_configs'])
    )
      throw new Error();
    const actions = input.value.action_requests,
      configs = input.value.review_configs;
    if (
      !Array.isArray(actions) ||
      !actions.length ||
      actions.length > 20 ||
      !Array.isArray(configs) ||
      configs.length !== actions.length
    )
      throw new Error();
    let total = 0,
      metadata = 0;
    for (const [i, action] of actions.entries()) {
      const config = configs[i];
      if (
        !plainRecord(action) ||
        !exactKeys(action, ['name', 'args', 'description']) ||
        !mutationArgs(action.name, action.args) ||
        typeof action.description !== 'string' ||
        action.description.length > MAX_DESCRIPTION ||
        !plainRecord(config) ||
        !exactKeys(config, ['action_name', 'allowed_decisions']) ||
        config.action_name !== action.name ||
        !Array.isArray(config.allowed_decisions) ||
        !config.allowed_decisions.length ||
        config.allowed_decisions.length > 4 ||
        new Set(config.allowed_decisions).size !==
          config.allowed_decisions.length ||
        config.allowed_decisions.some(
          (v) => !['approve', 'edit', 'reject', 'respond'].includes(v)
        )
      )
        throw new Error();
      metadata += action.description.length;
      total += Object.values(
        action.args as Record<string, unknown>
      ).reduce<number>((n, v) => n + (typeof v === 'string' ? v.length : 0), 0);
      if (total > MAX_TOTAL_TEXT || metadata > MAX_METADATA) throw new Error();
    }
    const choices = (['approve', 'reject'] as Choice[]).filter((choice) =>
      configs.every((c) => c.allowed_decisions.includes(choice))
    );
    return Object.freeze({
      kind: 'valid',
      interruptId: input.id,
      actions: actions as readonly ApprovalAction[],
      choices: Object.freeze(choices),
      signature: JSON.stringify(input),
    });
  } catch {
    return Object.freeze({
      kind: 'unavailable',
      reason:
        rawProposal === undefined
          ? 'Raw current proposal is unavailable: non-JSON data or visible bounds exceeded. No decisions can be sent.'
          : 'Complete raw proposal shown: unsupported action, arguments, metadata, shape or bounds. No decisions can be sent.',
      ...(rawProposal !== undefined ? { rawProposal } : {}),
    });
  }
}
/** Match the captured pause against current authority before calling this. */
export function createDecision(
  batch: ApprovalState,
  currentSignature: string,
  choice: unknown
): PlainValue | undefined {
  try {
    // A signature contains JSON escapes in addition to the raw bounded text.
    const captured = copyJson(batch, false, 33554432);
    if (
      (choice !== 'approve' && choice !== 'reject') ||
      !plainRecord(captured) ||
      captured.kind !== 'valid' ||
      typeof currentSignature !== 'string' ||
      currentSignature.length > (MAX_METADATA + MAX_TOTAL_TEXT) * 6 ||
      captured.signature !== currentSignature
    )
      return undefined;
    const current = approvalState(JSON.parse(currentSignature));
    if (
      current.kind !== 'valid' ||
      !sameJson(current, captured) ||
      !current.choices.includes(choice)
    )
      return undefined;
    return Object.freeze({
      decisions: Object.freeze(
        current.actions.map(() => Object.freeze({ type: choice }))
      ),
    });
  } catch {
    return undefined;
  }
}
approval-panel.tsx
import type { ApprovalState, Choice } from './approval-state';
import type { WorkspaceState } from './workspace-state';
export function ApprovalPanel({
  approval,
  workspace,
  busy,
  onDecision,
}: {
  approval: ApprovalState | null;
  workspace: WorkspaceState;
  busy: boolean;
  onDecision: (choice: Choice) => void;
}) {
  if (!approval) return null;
  if (approval.kind === 'unavailable')
    return (
      <section className="approval-panel" aria-label="Unavailable proposal">
        <h2>Proposal unavailable</h2>
        <p>{approval.reason}</p>
        {approval.rawProposal !== undefined && (
          <pre>{JSON.stringify(approval.rawProposal, null, 2)}</pre>
        )}
        <p>Start a new conversation to continue.</p>
      </section>
    );
  const paths = new Set(
    workspace.kind === 'valid' ? workspace.files.map((x) => x.path) : []
  );
  return (
    <section className="approval-panel" aria-label="Awaiting approval">
      <h2>
        Awaiting approval · {approval.actions.length} proposed{' '}
        {approval.actions.length === 1 ? 'action' : 'actions'}
      </h2>
      <p>
        Review the entire ordered batch. These are proposed arguments, not saved
        file changes.
      </p>
      <ol>
        {approval.actions.map((action, index) => {
          const path = String(action.args.file_path);
          return (
            <li key={index}>
              <h3>
                {action.name} · {path}
              </h3>
              {action.name === 'write_file' ? (
                <>
                  <p>
                    {paths.has(path)
                      ? 'Replacement proposal'
                      : 'New file proposal · not saved'}
                  </p>
                  <pre>{String(action.args.content)}</pre>
                </>
              ) : action.name === 'edit_file' ? (
                <>
                  <p>
                    Edit intent · replace_all:{' '}
                    {String(action.args.replace_all ?? false)}
                  </p>
                  <h4>Old text</h4>
                  <pre>{String(action.args.old_string)}</pre>
                  <h4>New text</h4>
                  <pre>{String(action.args.new_string)}</pre>
                </>
              ) : (
                <p>
                  Delete intent · {path === '/' ? 'root and descendants' : path}{' '}
                  (file or directory subtree)
                </p>
              )}
              <details>
                <summary>Raw proposal description</summary>
                <pre>{action.description}</pre>
              </details>
            </li>
          );
        })}
      </ol>
      <div className="decision-controls">
        {approval.choices.map((choice) => (
          <button
            type="button"
            key={choice}
            disabled={busy}
            onClick={() => onDecision(choice)}
          >
            {choice === 'approve'
              ? 'Approve entire batch'
              : 'Reject entire batch'}
          </button>
        ))}
      </div>
      {!approval.choices.length && (
        <p>
          No whole-batch Approve or Reject decision is permitted. Start a new
          conversation.
        </p>
      )}
    </section>
  );
}

Confirm what was saved

A tool result received is separate from saved workspace authority. Pause confirmation checks the native paused outcome, the exact current root checkpoint and history head, the owned human request and retained canonical message prefix, and the matching current interrupt batch. Terminal confirmation checks native success and a terminal root checkpoint with no pending tasks or interrupts. File contents come from that actual checkpoint, not from tool arguments, model prose or a predicted effect.

After a decision, the example reconciles the actual saved state. Approval does not guarantee a file will be created; a tool can fail. Rejection can lead to another proposal, which needs a new confirmed approval. It does not guarantee a terminal response, absence of the file or prevention of future writes.

application.ts
import type {
  AgentSession,
  AgentSnapshot,
  CompleteOutcome,
  Message,
  ToolCall,
} from '@threadplane/core';
import {
  createMessageContent,
  type MessageRow,
} from '@threadplane/content/messages';
import {
  copyJson,
  inspectJson,
  ownValue,
  plainRecord,
  workspaceState,
  type WorkspaceState,
  type PlainValue,
} from './workspace-state';
import {
  approvalState,
  createDecision,
  type ApprovalState,
  type Choice,
} from './approval-state';
import {
  captureAuthority,
  captureBaseline,
  captureTurn,
  captureResume,
  checkpointSource,
  sameObservedAuthority,
  type Authority,
  type AuthorityInput,
  type Checkpoint,
  type FilesystemSnapshot,
  type Turn,
} from './authority';
export interface FilesystemSession extends Omit<AgentSession, 'getSnapshot'> {
  getSnapshot(): AgentSnapshot & FilesystemSnapshot;
  load(options?: { readonly signal?: AbortSignal }): Promise<void>;
  resume(
    value: PlainValue,
    options?: { readonly signal?: AbortSignal }
  ): Promise<CompleteOutcome>;
}
export interface FilesystemClient {
  createThread(signal: AbortSignal): Promise<string>;
  sessionFactory(threadId: string): FilesystemSession;
  readCurrent(threadId: string, signal: AbortSignal): Promise<unknown>;
  readCheckpoint(source: Checkpoint, signal: AbortSignal): Promise<unknown>;
}
type Workspace = Exclude<WorkspaceState, { kind: 'invalid' }>;
export interface FilesystemApplicationSnapshot {
  readonly threadId: string | null;
  readonly rows: readonly MessageRow[];
  readonly messages: readonly Message[];
  readonly toolCalls: readonly ToolCall[];
  readonly observedWorkspace: Workspace;
  readonly savedWorkspace: Workspace;
  readonly approval: ApprovalState | null;
  readonly decisionToken: string | null;
  readonly phase:
    | 'idle'
    | 'working'
    | 'confirming'
    | 'paused'
    | '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 = (): FilesystemApplicationSnapshot =>
  Object.freeze({
    threadId: null,
    rows: Object.freeze([]),
    messages: Object.freeze([]),
    toolCalls: Object.freeze([]),
    observedWorkspace: Object.freeze({
      kind: 'missing',
      files: Object.freeze([]),
    }),
    savedWorkspace: Object.freeze({
      kind: 'missing',
      files: Object.freeze([]),
    }),
    approval: null,
    decisionToken: null,
    phase: 'idle',
    busy: false,
    canSubmit: true,
    outcome: null,
    notice: null,
    error: null,
    viewGeneration: 0,
  });
/** Native state remains transient. Copy canonical metadata only, never the raw file map. */
function metadata(source: FilesystemSnapshot): FilesystemSnapshot {
  inspectJson(source, true, [['values'], ['history', '0', 'values']]);
  const omit = (input: object, keys: string[]) =>
    Object.fromEntries(
      Object.keys(input)
        .filter((k) => !keys.includes(k))
        .map((k) => [k, ownValue(input, k)])
    );
  const history = ownValue(source, 'history');
  const copied = copyJson(
    {
      ...omit(source, ['values', 'history']),
      ...(Array.isArray(history)
        ? {
            history: history.map((x) => {
              if (!plainRecord(x)) throw Error('Unavailable history');
              return omit(x, ['values']);
            }),
          }
        : {}),
    },
    true
  ) as FilesystemSnapshot;
  return {
    ...copied,
    values: ownValue(source, 'values'),
    history: history as readonly unknown[] | undefined,
  };
}
export function createFilesystemApplication(client: FilesystemClient) {
  type Owner = {
    id: string;
    identity: string;
    session: FilesystemSession;
    content: ReturnType<typeof createMessageContent>;
    release?: () => void;
    closed: boolean;
    authority: Authority;
  };
  type Attempt = {
    controller: AbortController;
    owner?: Owner;
    generation: string;
    view: number;
    text: string;
    turn?: Turn;
    baseline?: ReturnType<typeof captureBaseline>;
    submitting?: boolean;
    saving?: boolean;
    previousHumanIds?: Set<string>;
    previousGenerations?: Set<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) =>
    !disposed &&
    attempt === run &&
    !run.controller.signal.aborted &&
    snapshot.viewGeneration === run.view &&
    (!run.owner || (owner === run.owner && !run.owner.closed));
  function publish(update: Partial<FilesystemApplicationSnapshot>) {
    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 task = Promise.race([
      Promise.resolve()
        .then(() => selected.session.dispose())
        .catch(() => undefined),
      new Promise<void>((resolve) => {
        timer = setTimeout(resolve, 2000);
      }),
    ]).finally(() => clearTimeout(timer));
    cleanups.add(task);
    void task.then(() => cleanups.delete(task));
    return task;
  }
  function observe(run: Attempt) {
    if (!run.owner || !current(run)) return;
    const source = run.owner.session.getSnapshot();
    if (!current(run)) return;
    const state = metadata(source);
    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.submitting && !run.saving && run.baseline) {
      const humans = state.messages.filter(
        (m) => m.role === 'user' && !run.previousHumanIds?.has(m.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: run.owner.identity,
          generation: run.generation,
          threadId: run.owner.id,
          humanId: human.id,
          humanContent: run.text,
        });
    }
    const workspace = workspaceState(state.values);
    const rows = run.owner.content.project(state as AgentSnapshot);
    if (!current(run)) return;
    publish({
      rows,
      messages: state.messages,
      toolCalls: state.toolCalls,
      ...(workspace.kind === 'invalid'
        ? {
            notice:
              'The current files update is unavailable. The last confirmed workspace is retained.',
          }
        : { observedWorkspace: workspace, notice: null }),
    });
    return state;
  }
  function finish(
    run: Attempt,
    phase: FilesystemApplicationSnapshot['phase'],
    outcome: CompleteOutcome | null,
    error: string | null,
    canSubmit = false
  ) {
    if (!current(run)) return;
    attempt = undefined;
    publish({ busy: false, phase, outcome, error, canSubmit });
  }
  function input(
    run: Attempt,
    state: FilesystemSnapshot,
    raw: unknown,
    outcome: string,
    checkpoint: unknown
  ): AuthorityInput {
    return {
      turn: run.turn,
      owner: run.owner!.identity,
      generation: run.generation,
      threadId: run.owner!.id,
      outcome,
      observedCheckpoint: checkpoint,
      observedValues: plainRecord(raw) ? ownValue(raw, 'values') : undefined,
      loadedCheckpoint: raw,
      snapshot: state,
    };
  }
  async function confirm(run: Attempt, outcome: CompleteOutcome) {
    if (!run.owner || !current(run)) return false;
    const selected = run.owner;
    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 state = observe(run);
    const checkpoint =
      state &&
      checkpointSource(
        plainRecord(state.history?.[0])
          ? ownValue(state.history![0] as object, 'checkpoint')
          : undefined,
        selected.id
      );
    if (!state || !checkpoint || !run.turn)
      throw Error('Unavailable exact checkpoint');
    const raw = await client.readCheckpoint(checkpoint, run.controller.signal);
    if (!current(run)) return false;
    const latest = observe(run);
    if (!latest || !current(run)) return false;
    const authority = captureAuthority(
      input(run, latest, raw, outcome, checkpoint)
    );
    if (!current(run)) return false;
    if (authority.kind === 'unconfirmed') {
      const proposal =
        latest.interrupts.length === 1
          ? approvalState(latest.interrupts[0])
          : null;
      publish({ approval: proposal?.kind === 'unavailable' ? proposal : null });
      throw Error('Unconfirmed authority');
    }
    selected.authority = authority;
    publish({
      savedWorkspace: authority.workspace,
      observedWorkspace: authority.workspace,
      approval: authority.kind === 'paused' ? authority.approval : null,
      decisionToken: authority.kind === 'paused' ? authority.signature : null,
    });
    finish(
      run,
      authority.kind === 'paused' ? 'paused' : 'saved',
      outcome,
      null,
      authority.kind === 'terminal'
    );
    return true;
  }
  function fail(run: Attempt, outcome: CompleteOutcome | null) {
    if (!current(run)) return;
    publish({
      approval:
        snapshot.approval?.kind === 'unavailable' ? snapshot.approval : null,
      decisionToken: null,
    });
    finish(
      run,
      outcome === 'success' || outcome === 'paused' ? 'unconfirmed' : 'failed',
      outcome ?? 'error',
      'The request or saved workspace could not be confirmed. The last confirmed workspace is retained. Start a new conversation to continue.'
    );
  }
  async function submit(text: string) {
    if (disposed || snapshot.busy || !snapshot.canSubmit || !text.trim())
      return false;
    const run: Attempt = {
      controller: new AbortController(),
      generation: crypto.randomUUID(),
      view: snapshot.viewGeneration,
      text,
    };
    attempt = run;
    publish({
      busy: true,
      canSubmit: false,
      phase: 'working',
      outcome: null,
      error: null,
      approval: null,
    });
    let outcome: CompleteOutcome | null = null;
    try {
      if (!current(run)) return false;
      if (!owner) {
        const id = await client.createThread(run.controller.signal);
        if (!current(run)) return false;
        if (typeof id !== 'string' || !id) throw Error('Unconfirmed creation');
        const selected: Owner = {
          id,
          identity: crypto.randomUUID(),
          session: client.sessionFactory(id),
          content: createMessageContent(),
          closed: false,
          authority: { kind: 'unconfirmed' },
        };
        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 {
              /* Reconciliation fails 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;
      run.baseline = captureBaseline(
        selected.authority.kind === 'terminal'
          ? { messages: selected.authority.messages }
          : undefined
      );
      if (!run.baseline) throw Error('Unavailable prefix');
      if (selected.authority.kind === 'terminal') {
        const raw = await client.readCurrent(
          selected.id,
          run.controller.signal
        );
        if (!current(run)) return false;
        if (
          !plainRecord(raw) ||
          !plainRecord(ownValue(raw, 'values')) ||
          !checkpointSource(ownValue(raw, 'checkpoint'), selected.id)
        )
          throw Error('Foreign current state');
        const latest = ownValue(raw, 'checkpoint');
        if (
          JSON.stringify(latest) !==
          JSON.stringify(selected.authority.checkpoint)
        )
          throw Error('Conversation advanced');
      }
      const before = selected.session.getSnapshot();
      if (!current(run)) return false;
      run.previousHumanIds = new Set(
        before.messages.filter((m) => m.role === 'user').map((m) => m.id)
      );
      run.previousGenerations = new Set(
        before.messages.map((m) => m.delivery.generation)
      );
      run.submitting = true;
      outcome = await selected.session.submit(text, {
        signal: run.controller.signal,
      });
      if (!current(run)) return false;
      observe(run);
      if (!current(run)) return false;
      if (outcome !== 'success' && outcome !== 'paused')
        throw Error('Unconfirmed outcome');
      return await confirm(run, outcome);
    } catch {
      fail(run, outcome);
      return false;
    }
  }
  async function decide(choice: Choice, decisionToken: string | null) {
    const selected = owner,
      authority = selected?.authority;
    if (
      disposed ||
      snapshot.busy ||
      snapshot.phase !== 'paused' ||
      !selected ||
      authority?.kind !== 'paused' ||
      !decisionToken ||
      decisionToken !== authority.signature ||
      snapshot.decisionToken !== decisionToken ||
      snapshot.approval !== authority.approval ||
      !authority.approval.choices.includes(choice)
    )
      return false;
    const run: Attempt = {
      controller: new AbortController(),
      generation: crypto.randomUUID(),
      view: snapshot.viewGeneration,
      text: authority.turn.humanContent,
      owner: selected,
    };
    run.turn = captureResume(authority.turn, run.generation);
    attempt = run;
    publish({ busy: true, canSubmit: false, phase: 'confirming', error: null });
    let outcome: CompleteOutcome | null = null;
    try {
      if (!current(run)) return false;
      const raw = await client.readCurrent(selected.id, run.controller.signal);
      if (!current(run)) return false;
      // This MUST be the current head. Reading the old checkpoint alone misses external advancement.
      const state = observe(run);
      if (
        !state ||
        !current(run) ||
        (snapshot as FilesystemApplicationSnapshot).phase !== 'confirming'
      )
        return false;
      const preflight: AuthorityInput = {
        ...input(
          run,
          state,
          raw,
          'paused',
          plainRecord(raw) ? ownValue(raw, 'checkpoint') : undefined
        ),
        turn: authority.turn,
        generation: authority.turn.generation,
      };
      if (!sameObservedAuthority(authority, preflight))
        throw Error('Pause advanced');
      const decision = createDecision(
        authority.approval,
        authority.approval.signature,
        choice
      );
      if (
        !decision ||
        !current(run) ||
        (snapshot as FilesystemApplicationSnapshot).phase !== 'confirming'
      )
        throw Error('Unavailable whole batch');
      publish({ phase: 'working', approval: null, decisionToken: null });
      if (!current(run)) return false;
      outcome = await selected.session.resume(decision, {
        signal: run.controller.signal,
      });
      if (!current(run)) return false;
      observe(run);
      if (!current(run)) return false;
      if (outcome !== 'success' && outcome !== 'paused')
        throw Error('Unconfirmed resume');
      return await confirm(run, outcome);
    } catch {
      fail(run, outcome);
      return false;
    }
  }
  async function stop() {
    const run = attempt;
    if (disposed || (!run && snapshot.phase !== 'paused')) return;
    attempt = undefined;
    run?.controller.abort();
    publish({
      busy: false,
      canSubmit: false,
      phase: 'stopped',
      approval: null,
      decisionToken: null,
      outcome: 'aborted',
      error:
        'Response stopped. The last confirmed workspace is retained. Cancellation does not roll back backend files. Start a new conversation to continue.',
    });
    try {
      await (run?.owner ?? owner)?.session.stop();
    } catch {
      /* Not rollback. */
    }
  }
  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,
    decide,
    stop,
    newConversation,
    dispose,
  };
}
authority.ts
import type { Message, ToolCall } from '@threadplane/core';
import {
  approvalState,
  mutationArgs,
  type ApprovalState,
} from './approval-state';
import {
  copyJson,
  inspectJson,
  ownValue,
  plainRecord,
  sameJson,
  workspaceState,
  type WorkspaceState,
} from './workspace-state';
 
export interface TurnIdentity {
  readonly owner: string;
  /** Local attempt identity, never a backend run id. */
  readonly generation: string;
  readonly threadId: string;
  readonly humanId: string;
  readonly humanContent: string;
  readonly backendRunId?: string;
}
export interface Baseline {
  readonly messages: readonly unknown[];
}
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 FilesystemSnapshot {
  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 AuthorityInput {
  readonly turn: Turn | undefined;
  readonly owner: string;
  readonly generation: string;
  readonly threadId: string;
  readonly backendRunId?: string;
  /** Native submit/resume result outcome, not server run status. */
  readonly outcome: string;
  readonly observedCheckpoint: unknown;
  readonly observedValues: unknown;
  /** State loaded from the exact observed checkpoint. */
  readonly loadedCheckpoint: unknown;
  readonly snapshot: FilesystemSnapshot;
}
interface Confirmed {
  readonly checkpoint: Checkpoint;
  readonly workspace: Exclude<WorkspaceState, { kind: 'invalid' }>;
  /** Actual saved wire messages for the next pre-send canonical prefix. */
  readonly messages: readonly unknown[];
  readonly turn: Turn;
  readonly signature: string;
}
export type Authority =
  | { readonly kind: 'unconfirmed' }
  | (Confirmed & { readonly kind: 'terminal' })
  | (Confirmed & {
      readonly kind: 'paused';
      readonly approval: Extract<ApprovalState, { kind: 'valid' }>;
    });
 
export function captureBaseline(input?: unknown): Baseline | undefined {
  try {
    const values = input === undefined ? { messages: [] } : input;
    inspectJson(values, false, [[]]);
    if (!plainRecord(values)) return undefined;
    const messages = copyJson(ownValue(values, 'messages'));
    if (!Array.isArray(messages)) return undefined;
    return Object.freeze({ messages });
  } catch {
    return undefined;
  }
}
export function captureTurn(
  baseline: Baseline | undefined,
  input: TurnIdentity
): Turn | undefined {
  try {
    const identity = copyJson(input, true),
      prefix = copyJson(baseline);
    if (
      !plainRecord(identity) ||
      !plainRecord(prefix) ||
      !Array.isArray(prefix.messages) ||
      ['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)) ||
      prefix.messages.some((m) => plainRecord(m) && m.id === identity.humanId)
    )
      return undefined;
    return Object.freeze({ ...identity, baseline: prefix }) as unknown as Turn;
  } catch {
    return undefined;
  }
}
/** A resume is another attempt of the original human turn, not a new human. */
export function captureResume(
  turn: Turn | undefined,
  generation: string
): Turn | undefined {
  if (!turn || !generation || generation === turn.generation) return undefined;
  return captureTurn(turn.baseline, { ...turn, generation });
}
export function checkpointSource(
  input: unknown,
  threadId: string
): Checkpoint | undefined {
  try {
    const cp = copyJson(input);
    if (
      !plainRecord(cp) ||
      cp.thread_id !== threadId ||
      cp.checkpoint_ns !== '' ||
      typeof cp.checkpoint_id !== 'string' ||
      !cp.checkpoint_id ||
      Object.keys(cp).some(
        (k) =>
          ![
            'thread_id',
            'checkpoint_ns',
            'checkpoint_id',
            'checkpoint_map',
          ].includes(k)
      )
    )
      return undefined;
    if (
      Object.hasOwn(cp, 'checkpoint_map') &&
      (!plainRecord(cp.checkpoint_map) ||
        Object.keys(cp.checkpoint_map).length !== 1 ||
        cp.checkpoint_map[''] !== cp.checkpoint_id)
    )
      return undefined;
    return cp as unknown as Checkpoint;
  } catch {
    return undefined;
  }
}
const empty = (v: unknown) =>
  v === undefined || (Array.isArray(v) && v.length === 0);
const clean = (r: Record<string, unknown>) =>
  (r.error === undefined || r.error === null) &&
  ['subgraphs', '__interrupt__', 'interrupts'].every((k) => empty(r[k]));
const except = (v: unknown, omit: string[]): Record<string, unknown> => {
  if (!plainRecord(v)) throw new Error('Expected record');
  return Object.fromEntries(
    Object.keys(v)
      .filter((k) => !omit.includes(k))
      .map((k) => [k, ownValue(v, k)])
  );
};
const projectedValues = (v: Record<string, unknown>) =>
  except(v, ['messages', '__interrupt__']);
const withoutTransient = (v: Record<string, unknown>) =>
  except(v, ['__interrupt__']);
const pauseNext = ['HumanInTheLoopMiddleware.after_model'];
 
/** Files authority is checkpoint data, never a ToolMessage repr or prediction.
 * A failed confirmation returns no replacement; callers retain prior authority. */
export function captureAuthority(input: AuthorityInput): Authority {
  const unconfirmed: Authority = Object.freeze({ kind: 'unconfirmed' });
  try {
    inspectJson(input.loadedCheckpoint, false, [['values']]);
    inspectJson(input.observedValues, false, [[]]);
    inspectJson(input.snapshot, true, [['values'], ['history', '0', 'values']]);
    const sourceValues = ownValue(input.loadedCheckpoint as object, 'values');
    const workspace = workspaceState(sourceValues);
    if (workspace.kind === 'invalid') return unconfirmed;
    const historySource = ownValue(input.snapshot, 'history');
    if (historySource !== undefined && !Array.isArray(historySource))
      return unconfirmed;
    const history = Array.isArray(historySource)
      ? Array.from(
          { length: ownValue(historySource, 'length') as number },
          (_, i) => except(ownValue(historySource, String(i)), ['values'])
        )
      : undefined;
    const latestSource = Array.isArray(historySource)
      ? ownValue(historySource, '0')
      : undefined;
    const turn = copyJson(input.turn, true),
      state = copyJson(
        {
          ...except(input.snapshot, ['values', 'history']),
          ...(history === undefined ? {} : { history }),
        },
        true
      ),
      raw = copyJson(except(input.loadedCheckpoint, ['values'])),
      observed = input.observedValues;
    const otherValues = copyJson(except(sourceValues, ['files']));
    if (!plainRecord(otherValues)) return unconfirmed;
    const values: Record<string, unknown> = {
      ...otherValues,
      ...(plainRecord(sourceValues) && Object.hasOwn(sourceValues, 'files')
        ? { files: ownValue(sourceValues, 'files') }
        : {}),
    };
    const paused = input.outcome === 'paused';
    if (
      (!paused && input.outcome !== 'success') ||
      !plainRecord(turn) ||
      !plainRecord(turn.baseline) ||
      !Array.isArray(turn.baseline.messages) ||
      !plainRecord(state) ||
      !plainRecord(raw) ||
      !plainRecord(observed) ||
      turn.owner !== input.owner ||
      turn.generation !== input.generation ||
      turn.threadId !== input.threadId ||
      state.status !== 'idle' ||
      (state.error !== undefined && state.error !== null) ||
      !Array.isArray(state.subgraphs) ||
      state.subgraphs.length ||
      !Array.isArray(state.interrupts) ||
      (raw.error !== undefined && raw.error !== null) ||
      !empty(raw.subgraphs)
    )
      return unconfirmed;
    const runId = input.backendRunId ?? turn.backendRunId;
    if (
      (turn.backendRunId !== undefined &&
        input.backendRunId !== undefined &&
        turn.backendRunId !== input.backendRunId) ||
      (runId !== undefined &&
        (typeof runId !== 'string' ||
          !runId ||
          (raw.run_id !== undefined && raw.run_id !== runId) ||
          (plainRecord(raw.metadata) &&
            raw.metadata.run_id !== undefined &&
            raw.metadata.run_id !== runId)))
    )
      return unconfirmed;
    const checkpoint = checkpointSource(raw.checkpoint, input.threadId),
      requested = checkpointSource(input.observedCheckpoint, input.threadId);
    const latest = Array.isArray(state.history) ? state.history[0] : undefined;
    const next = paused ? pauseNext : [];
    if (
      !checkpoint ||
      !requested ||
      !sameJson(checkpoint, requested) ||
      !plainRecord(latest) ||
      !clean(latest) ||
      !sameJson(latest.next, next) ||
      !sameJson(
        checkpointSource(latest.checkpoint, input.threadId),
        checkpoint
      ) ||
      !sameJson(raw.next, next) ||
      !Array.isArray(raw.tasks) ||
      (latest.tasks !== undefined && !sameJson(latest.tasks, raw.tasks)) ||
      (plainRecord(latestSource) &&
        ownValue(latestSource, 'values') !== undefined &&
        !sameJson(ownValue(latestSource, 'values'), values))
    )
      return unconfirmed;
    if (
      !plainRecord(values) ||
      !clean(values) ||
      !sameJson(values, withoutTransient(observed)) ||
      !sameJson(projectedValues(values), ownValue(input.snapshot, 'values')) ||
      !Array.isArray(values.messages) ||
      !Array.isArray(state.messages) ||
      !Array.isArray(state.toolCalls)
    )
      return unconfirmed;
    let approval: Extract<ApprovalState, { kind: 'valid' }> | undefined;
    if (paused) {
      if (state.interrupts.length !== 1 || raw.tasks.length !== 1)
        return unconfirmed;
      const task = raw.tasks[0];
      if (
        !plainRecord(task) ||
        typeof task.id !== 'string' ||
        !task.id ||
        task.name !== pauseNext[0] ||
        !Array.isArray(task.interrupts) ||
        task.interrupts.length !== 1 ||
        (task.error !== undefined && task.error !== null) ||
        (task.state !== undefined && task.state !== null) ||
        !empty(task.subgraphs) ||
        !sameJson(task.interrupts, state.interrupts) ||
        (ownValue(observed, '__interrupt__') !== undefined &&
          !sameJson(ownValue(observed, '__interrupt__'), state.interrupts))
      )
        return unconfirmed;
      const proposal = approvalState(state.interrupts[0]);
      if (proposal.kind !== 'valid') return unconfirmed;
      approval = proposal;
    } else if (
      state.interrupts.length ||
      raw.tasks.length ||
      !clean(raw) ||
      !empty(ownValue(observed, '__interrupt__'))
    )
      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 ||
      values.messages
        .slice(prefix.length + 1)
        .some((m) => plainRecord(m) && m.type === 'human')
    )
      return unconfirmed;
    if (
      !canonicalMatches(
        values.messages,
        state.messages,
        state.toolCalls,
        approval
      )
    )
      return unconfirmed;
    if (!paused) {
      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;
    }
    const confirmed = {
      checkpoint,
      workspace,
      messages: values.messages,
      turn: turn as unknown as Turn,
      signature: JSON.stringify({
        checkpoint,
        turn,
        workspace,
        otherValues: except(values, ['files']),
        messages: state.messages,
        tools: state.toolCalls,
        interrupts: state.interrupts,
      }),
    };
    return approval
      ? Object.freeze({ ...confirmed, kind: 'paused', approval })
      : Object.freeze({ ...confirmed, kind: 'terminal' });
  } catch {
    return unconfirmed;
  }
}
 
/** Canonical native projections must match every raw id, call, argument, result,
 * text and delivery. Pending calls may exist only in the final paused AI step. */
function canonicalMatches(
  raw: unknown[],
  messages: unknown[],
  tools: unknown[],
  approval?: Extract<ApprovalState, { kind: 'valid' }>
): boolean {
  if (raw.length !== messages.length) return false;
  const catalog = new Map<string, Record<string, unknown>>(),
    ids = new Set<string>(),
    seen = new Set<string>();
  for (const t of tools) {
    if (
      !plainRecord(t) ||
      typeof t.id !== 'string' ||
      !t.id ||
      catalog.has(t.id) ||
      !['complete', 'pending'].includes(t.status as string)
    )
      return false;
    catalog.set(t.id, t);
  }
  const pending = new Map<string, Record<string, unknown>>();
  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 ||
      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) ||
      !plainRecord(m.delivery) ||
      m.delivery.phase !== 'complete' ||
      m.delivery.generation !== m.id
    )
      return false;
    ids.add(wire.id);
    const extra = wire.additional_kwargs ?? {};
    if (
      !plainRecord(extra) ||
      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 calls = wire.tool_calls ?? [],
        projected = m.toolCallIds ?? [];
      if (
        !Array.isArray(calls) ||
        !Array.isArray(projected) ||
        calls.length !== projected.length
      )
        return false;
      for (const [j, c] of calls.entries()) {
        if (
          !plainRecord(c) ||
          typeof c.id !== 'string' ||
          !c.id ||
          c.id !== projected[j] ||
          seen.has(c.id) ||
          typeof c.name !== 'string' ||
          !c.name ||
          !plainRecord(c.args) ||
          (c.type !== undefined && c.type !== 'tool_call') ||
          Object.keys(c).some(
            (k) => !['id', 'name', 'args', 'type'].includes(k)
          )
        )
          return false;
        const t = catalog.get(c.id);
        if (!t || t.name !== c.name || !sameJson(t.args, c.args)) return false;
        seen.add(c.id);
        pending.set(c.id, c);
      }
      const pausedMessage =
        !!approval && i === raw.length - 1 && calls.length > 0;
      if (m.delivery.outcome !== (pausedMessage ? 'paused' : 'success'))
        return false;
    } else if (wire.type === 'tool') {
      const t = catalog.get(wire.tool_call_id as string);
      if (
        !t ||
        !pending.delete(wire.tool_call_id as string) ||
        t.status !== 'complete' ||
        t.result !== wire.content ||
        !['success', 'error'].includes(wire.status as string) ||
        (wire.name !== t.name &&
          !(
            wire.status === 'error' &&
            (wire.name === undefined || wire.name === null)
          )) ||
        !empty(wire.tool_calls) ||
        !empty(m.toolCallIds) ||
        m.delivery.outcome !== 'success'
      )
        return false;
    } else if (
      pending.size ||
      !empty(wire.tool_calls) ||
      !empty(m.toolCallIds) ||
      wire.tool_call_id !== undefined ||
      m.delivery.outcome !== 'success'
    )
      return false;
  }
  if (seen.size !== catalog.size) return false;
  if (!approval) return pending.size === 0;
  if (
    !pending.size ||
    [...pending.keys()].some(
      (id) =>
        catalog.get(id)?.status !== 'pending' ||
        Object.hasOwn(catalog.get(id)!, 'result')
    )
  )
    return false;
  const protectedCalls: Record<string, unknown>[] = [];
  for (const call of pending.values()) {
    if (['write_file', 'edit_file', 'delete'].includes(call.name as string)) {
      if (!mutationArgs(call.name, call.args)) return false;
      const path = (call.args as Record<string, string>).file_path;
      // Pinned graph permission: exact write/edit descendants; recursive delete
      // checks subtree overlap with the /reports anchor.
      if (
        path.startsWith('/reports/') ||
        (call.name === 'delete' && (path === '/reports' || path === '/'))
      )
        protectedCalls.push(call);
    } else if (
      ![
        'ls',
        'read_file',
        'glob',
        'grep',
        'lookup_field_elevation',
        'lookup_runway_length',
      ].includes(call.name as string)
    )
      return false;
  }
  return (
    protectedCalls.length === approval.actions.length &&
    protectedCalls.every(
      (c, i) =>
        c.name === approval.actions[i].name &&
        sameJson(c.args, approval.actions[i].args)
    )
  );
}
export function sameObservedAuthority(
  captured: Authority,
  input: AuthorityInput
): boolean {
  if (captured.kind !== 'paused') return false;
  const current = captureAuthority(input);
  return (
    current.kind === 'paused' &&
    captured.signature === current.signature &&
    captured.approval.signature === current.approval.signature
  );
}

Stop and recover

Stop preserves the last confirmed workspace and requires New conversation before another Send. An uncertain confirmation also keeps the last confirmed state and requires New; it never reuses a stale approval. New, Stop, disposal and changes of connection or operation invalidate local decision ownership. Stopping protects the client view and is not a rollback of backend writes. There is no automatic retry, replay or resume.

Build from source

From a Threadplane checkout with dependencies installed:

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

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

Looking for something specific?