@juspay/neurolink
Version:
The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002
175 lines (174 loc) • 7.87 kB
TypeScript
/**
* Background commands (N4) — run a command detached, bank its output, monitor
* it — types.
*
* A long-running agent has to run real commands: a build, a test suite, a
* linter whose output is the evidence for a review finding. Two things make
* that safe and useful, and both are in these types.
*
* 1. **Nothing is discarded.** Both streams go straight to files and the FULL
* files are banked as artifacts when the command settles. What reaches the
* conversation is a bounded `tailPreview` plus a {@link BankedArtifactRef}
* per stream — a pointer, never a replacement. `maxOutputBytes` is the one
* bound, and hitting it is a loud state (`output-limit`), not a silent cut.
* 2. **Execution is a contract, not advice.** `argv` arrays only, no shell
* ever; an executable allowlist; a cwd that must resolve, through symlinks,
* inside a declared root; a timeout that actually kills.
*
* Naming: `cli.ts` already owns `CommandResult` / `CommandDefinition` and
* `action.ts` owns `ActionCommand`, so everything here carries the
* `BackgroundCommand` prefix (Critical Rule 9).
*/
import type { BankedArtifactRef } from "./artifact.js";
/** Which of a command's two output streams a page or artifact refers to. */
export type BackgroundCommandStreamName = "stdout" | "stderr";
/**
* Lifecycle of one background command.
*
* `queued` is the window between the handle being returned and the OS
* confirming the child started. The four settled states are deliberately
* distinct: "the command failed" and "we killed it because it printed 4 GB"
* are different facts about a run, and collapsing them loses the one a
* reviewer needs.
*/
export type BackgroundCommandState = "queued" | "running" | "exited" | "killed" | "timeout" | "output-limit";
/** `true` to allow, or a string giving the reason the command was refused. */
export type BackgroundCommandAllowDecision = true | string;
/**
* What a host permits. There is no default policy: without one every start is
* refused, because "run whatever the model asks" is not a defensible default
* for a primitive that executes processes.
*/
export type BackgroundCommandPolicy = {
/**
* Executables that may be started, matched EXACTLY against `argv[0]` — no
* basename fallback, so allowlisting `git` never permits `/tmp/evil/git`.
* Required, and an empty list refuses everything.
*/
allowedExecutables: string[];
/**
* Final say after the allowlist and the sandbox have passed. Return `true`
* to allow, or a string that is handed to the caller as the refusal reason
* (so put the recovery step in it).
*/
allowlist?: (argv: string[], cwd: string) => BackgroundCommandAllowDecision;
/**
* Sandbox root. The resolved REAL cwd (symlinks followed) must be this
* directory or inside it.
*/
cwdRoot: string;
/** Wall-clock budget when the caller names none. Default 120_000. */
defaultTimeoutMs?: number;
/** Per-stream byte cap when the caller names none. Default 10_485_760. */
maxOutputBytes?: number;
};
/**
* Per-start options. Only `timeoutMs` and `maxOutputBytes` fall back to the
* policy's defaults. The rest have their own omission behaviour: `env`
* inherits the parent environment, `label` defaults to `argv[0]`, `sessionId`
* resolves from the host's tool context, and `abortSignal` simply has no
* fallback.
*/
export type BackgroundCommandOptions = {
/** Working directory. Must resolve inside `BackgroundCommandPolicy.cwdRoot`. */
cwd: string;
/** Wall-clock budget in ms; SIGTERM then SIGKILL. */
timeoutMs?: number;
/** Per-stream byte cap; hitting it kills the command with `output-limit`. */
maxOutputBytes?: number;
/**
* Environment for the child. When given it REPLACES the parent environment
* rather than extending it — the command gets exactly these variables and
* nothing else. Omit it to inherit the parent environment, which is what a
* repository's own checks normally need.
*/
env?: Record<string, string>;
/** Short human label for logs and the banked artifacts. Defaults to argv[0]. */
label?: string;
/** Session the command belongs to; scopes the outstanding counters. */
sessionId?: string;
/** Parent cancellation — an aborted parent kills the command. */
abortSignal?: AbortSignal;
};
/** Returned the moment a command is accepted — before it has produced output. */
export type BackgroundCommandHandle = {
taskId: string;
argv: string[];
startedAt: number;
};
/**
* Everything known about one command right now.
*
* `stdout` / `stderr` appear once the command has settled and its streams have
* been banked; `tailPreview` is available throughout and is always bounded.
* The preview is for orientation — the banked artifacts are the evidence.
*/
export type BackgroundCommandStatus = {
taskId: string;
/** Short human label, e.g. "git log" — which command this is. */
label: string;
state: BackgroundCommandState;
/** Process exit code, once it exited on its own. */
exitCode?: number;
/** Signal that ended the process, when one did. */
signal?: string;
durationMs: number;
/** Bytes written to each stream's log file so far. */
stdoutBytes: number;
stderrBytes: number;
/** The FULL stdout, banked (N3). Present once settled. */
stdout?: BankedArtifactRef;
/** The FULL stderr, banked (N3). Present once settled. */
stderr?: BankedArtifactRef;
/** Tail of the output so far, ≤ 2000 chars. Never a substitute for the files. */
tailPreview: string;
/** Why the command could not run, or how it was cut short. */
error?: string;
};
/** One character window of a command's output, read straight from the log file. */
export type BackgroundCommandOutputPage = {
taskId: string;
stream: BackgroundCommandStreamName;
content: string;
offset: number;
limit: number;
/** Characters in the whole stream, including what this page did not return. */
totalSize: number;
hasMore: boolean;
};
/** Character window for a paginated output read. */
export type BackgroundCommandPageRequest = {
stream: BackgroundCommandStreamName;
/** Character offset to start at. Default 0. */
offset?: number;
/** Maximum characters to return. Default 50_000, hard cap 200_000. */
limit?: number;
};
/**
* Outstanding commands for a session: `running` have not settled yet, and
* `finished` have settled without anyone having looked at them since.
*
* "Not looked at" rather than "ever finished" is what makes the number
* actionable — reading a settled command's status or output clears it, so a
* non-zero `finished` always means there is something new to read. Nothing is
* discarded when it clears: the job, its logs and its artifacts stay exactly
* where they were.
*
* Carried on every command tool result and — via the checklist — on every
* `tasks_list`, so the model learns a build finished without polling.
*/
export type BackgroundCommandCounts = {
running: number;
finished: number;
};
/** Supplies {@link BackgroundCommandCounts} to the task checklist. */
export type BackgroundCommandCountsSource = (sessionId: string) => BackgroundCommandCounts;
/** What `run_command_bg` returns: the handle plus the outstanding counters. */
export type BackgroundCommandStartToolResult = BackgroundCommandHandle & BackgroundCommandCounts;
/** What the monitor tools return: a status plus the outstanding counters. */
export type BackgroundCommandStatusToolResult = BackgroundCommandStatus & BackgroundCommandCounts;
/** Refusal shape shared with the agent tool registrar: recovery text included. */
export type BackgroundCommandRefusal = {
isError: true;
error: string;
};