Saltar al contenido principal
Versión: 0.13.1

Interaction port

An InteractionPort is the UI side of a checkpoint: how a host shows a question and reads a person's answer. It lives in the core, so the checkpoints never depend on a terminal.

interface ApprovalPrompt {
title: string;
lines: string[];
/** The question in a plain terminal; defaults to the approve/reject one. */
question?: string;
}

interface InteractionPort {
/** A step-gate or approval checkpoint; resolves to the raw answer ("", "y", "n", "q"…). */
askDecision(prompt: ApprovalPrompt, signal: AbortSignal): Promise<string>;
/** Waits until the person confirms they intervened by hand (e.g. logged in). */
askManualIntervention(prompt: ApprovalPrompt, signal: AbortSignal): Promise<string>;
/** A one-way message for the person. */
notify(message: string): void;
}

One port is installed per process: setInteractionPort(port) and getInteractionPort().

The ports the kit installs​

WhenPortHow it asks
Importing the packageterminalInteractionPortPrints the checkpoint and asks with readline (on the chat's own interface when runChatTui() is running).
While runChatInk() or createProgressView() runsthe Ink portA panel inside the Ink view; restored to the previous port when the chat or view ends.

Without a TTY the terminal port prints nothing and never answers, so the response file is the only channel.

Rules for a port​

  • Resolve with the raw answer: "y", "n", "q", ""… The kit lowercases and trims it.
  • Never resolve if you can't ask. Return a promise that stays pending (for example, when no window is open) so the response file can win.
  • Honor signal. It aborts when the other channel answered first: close your dialog and leave the UI usable. Don't resolve after an abort.
  • Don't throw for normal cases. A rejected promise is treated like "can't ask": the file channel stays in the race.

A port for an Electron app​

The main process runs the agent (the core has no terminal code), and asks the renderer through IPC:

main/interaction.ts
import { ipcMain, type BrowserWindow } from "electron";
import { randomUUID } from "node:crypto";
import { setInteractionPort, type ApprovalPrompt, type InteractionPort } from "@falkenslab/agent-kit";

export function installWindowPort(window: BrowserWindow): void {
const ask = (kind: "decision" | "manual", prompt: ApprovalPrompt, signal: AbortSignal) =>
new Promise<string>((resolve) => {
if (window.isDestroyed()) return; // can't ask: let the response file answer
const id = randomUUID();
const onAnswer = (_event: unknown, answer: { id: string; value: string }) => {
if (answer.id !== id) return;
ipcMain.off("checkpoint:answer", onAnswer);
resolve(answer.value);
};
ipcMain.on("checkpoint:answer", onAnswer);
signal.addEventListener(
"abort",
() => {
ipcMain.off("checkpoint:answer", onAnswer);
window.webContents.send("checkpoint:cancel", { id });
},
{ once: true },
);
window.webContents.send("checkpoint:ask", { id, kind, prompt });
});

const port: InteractionPort = {
askDecision: (prompt, signal) => ask("decision", prompt, signal),
askManualIntervention: (prompt, signal) => ask("manual", prompt, signal),
notify: (message) => window.webContents.send("agent:notice", message),
};
setInteractionPort(port);
}

The renderer shows a dialog on checkpoint:ask, sends checkpoint:answer with "y", "n" or "q" (or "" for a manual intervention), and closes the dialog on checkpoint:cancel.

Answering through the file only​

setInteractionPort(null);

Every checkpoint then waits for <runDir>/approval-response.txt. Useful for tests and fully remote supervision.

Reusing the terminal port​

terminalInteractionPort is exported, with setSharedReadline(rl) and getSharedReadline(), for a program that has its own readline interface and wants checkpoints asked on it rather than on a second one fighting it for the keyboard.