mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 07:16:13 +00:00
* feat(desktop): keep an agentenv computer up and drive it through REA's MCP tools - A Python helper keeps one agentenv computer up per build with run_bundle(keep=True) and reports its address, MCP URL, control token and sandbox id; it tears the computer down when stdin closes. - AgentEnvComputer owns the helper, waits for readiness, holds one MCP session to the computer's computer_* tools, records through the computer-recording routes, and removes the sandbox's containers by label when agentenv cannot confirm teardown. The token never leaves it. - start_build_computer starts a computer from a request (archive package, apt repository or .deb, opened on demand with REA in /opt/rea) or from a bundle folder; act_on_build_computer forwards screenshot, click, drag, scroll, type, key and wait and returns the driver's images unchanged; close_build_computer tears it down. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(desktop): remove a build computer's container by name when agentenv cannot confirm teardown agentenv's local provider runs an ordinary computer as agent-<sandbox id> without the agentenv.sandbox label, so the label-only fallback found nothing and reported a running computer as removed. The fallback now lists containers by exact name and by label. The real test holds its recording open long enough for a first frame, checks the container by name, and runs rea inside the computer; the guide and tool descriptions say the app opens from the Applications menu and that a screenshot after opening a menu should come from a wait. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(desktop): own failed build-computer starts, keep ids past oversized screenshots, bound recordings - A start is owned from the helper's spawn. One that fails and whose teardown is unconfirmed stays held under a computer_id close can retry; closeAll refuses new starts, cancels and joins starts in progress, then closes every held computer. - When the first screenshot is too large, the error carries REA's result with the computer_id, and start_build_computer takes screenshot region, format and quality. - Recordings are refused before reading when the manifest's total exceeds the allowance, then fetched one segment at a time, each read bounded and checked against its size and sha256. - The helper keeps the tail of its output (a new tail retention mode in ProviderProcessSupervisor), so a build's own error is not crowded out. - A helper that reported no computer exits on its own before SIGTERM, so no phantom owner is kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(desktop): bound a build computer's action log Every action kept its full arguments for the computer's whole life, so a long session grew REA's memory without limit. The log now keeps the latest 256 actions within 256 KiB, cuts each string to 1 KiB and each list to 64 items (naming what it cut), and keeps the total and the last applied index of each kind of action, so rules over the whole session hold after entries drop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
156 lines
5.4 KiB
TypeScript
156 lines
5.4 KiB
TypeScript
import type { ComputerAction } from "../../domain/desktop/computerActions.js";
|
|
import type { JsonValue } from "../../domain/jsonValue.js";
|
|
import { boundedUtf8Text } from "../../process/boundedUtf8Text.js";
|
|
|
|
/** Bytes the log keeps of each string in an action's arguments, such as type's text. */
|
|
export const LOGGED_TEXT_BYTES = 1024;
|
|
/** Items the log keeps of each list in an action's arguments, such as drag's path. */
|
|
export const LOGGED_LIST_ITEMS = 64;
|
|
/** The latest actions the log keeps, and the JSON bytes they may take together. */
|
|
export const LOGGED_ACTIONS = 256;
|
|
export const LOGGED_BYTES = 256 * 1024;
|
|
|
|
type Outcome = "pending" | "applied" | "refused" | "failed";
|
|
|
|
/** One forwarded action, in the order REA sent it, as the witness reads it. */
|
|
export interface BuildComputerAction {
|
|
readonly index: number;
|
|
readonly action: ComputerAction;
|
|
readonly tool: string;
|
|
/** The driver's arguments as sent, keys translated, each string and list cut to the log's allowance. */
|
|
readonly arguments: Readonly<Record<string, JsonValue>>;
|
|
/** The arguments cut in this entry; the driver received them whole. */
|
|
readonly truncated_arguments?: readonly string[];
|
|
readonly started_at: string;
|
|
readonly ended_at: string | null;
|
|
readonly outcome: Outcome;
|
|
}
|
|
|
|
/** A value within the log's allowance, and whether it was cut. */
|
|
const bounded = (
|
|
value: JsonValue,
|
|
): { readonly value: JsonValue; readonly cut: boolean } => {
|
|
if (typeof value === "string") {
|
|
const { text, truncated } = boundedUtf8Text(value, LOGGED_TEXT_BYTES);
|
|
return { value: text, cut: truncated };
|
|
}
|
|
if (Array.isArray(value)) {
|
|
const items = value.slice(0, LOGGED_LIST_ITEMS).map(bounded);
|
|
return {
|
|
value: items.map((item) => item.value),
|
|
cut: value.length > items.length || items.some((item) => item.cut),
|
|
};
|
|
}
|
|
if (value === null || typeof value !== "object") return { value, cut: false };
|
|
const fields = Object.entries(value).map(
|
|
([name, field]) => [name, bounded(field)] as const,
|
|
);
|
|
return {
|
|
value: Object.fromEntries(
|
|
fields.map(([name, field]) => [name, field.value]),
|
|
),
|
|
cut: fields.some(([, field]) => field.cut),
|
|
};
|
|
};
|
|
|
|
const jsonBytes = (entry: BuildComputerAction): number =>
|
|
Buffer.byteLength(JSON.stringify(entry));
|
|
|
|
/**
|
|
* A build computer's action log. It counts every action and keeps the index
|
|
* of the last applied action of each kind, so rules over a whole session hold
|
|
* however long it runs, and keeps the latest actions themselves within
|
|
* `LOGGED_ACTIONS` and `LOGGED_BYTES`, dropping the oldest first.
|
|
*/
|
|
export class BuildComputerActionLog {
|
|
readonly #kept: { entry: BuildComputerAction; bytes: number }[] = [];
|
|
readonly #lastApplied: Partial<Record<ComputerAction, number>> = {};
|
|
#total = 0;
|
|
#bytes = 0;
|
|
|
|
/** Every action logged, kept or not; the next one's index. */
|
|
get total(): number {
|
|
return this.#total;
|
|
}
|
|
|
|
/** How many of the earliest actions are no longer kept. */
|
|
get dropped(): number {
|
|
return this.#total - this.#kept.length;
|
|
}
|
|
|
|
/** The kept actions, oldest first, indexed on from `dropped`. */
|
|
get entries(): readonly BuildComputerAction[] {
|
|
return this.#kept.map(({ entry }) => entry);
|
|
}
|
|
|
|
/** The index of the last applied action of each kind, kept or not. */
|
|
get lastApplied(): Readonly<Partial<Record<ComputerAction, number>>> {
|
|
return { ...this.#lastApplied };
|
|
}
|
|
|
|
/** Log an action as it is sent, pending, and how to record how it ended. */
|
|
begin(
|
|
action: ComputerAction,
|
|
tool: string,
|
|
args: Readonly<Record<string, JsonValue>>,
|
|
): {
|
|
readonly index: number;
|
|
readonly settle: (outcome: Exclude<Outcome, "pending">) => void;
|
|
} {
|
|
const index = this.#total;
|
|
const fields = Object.entries(args).map(
|
|
([name, value]) => [name, bounded(value)] as const,
|
|
);
|
|
const truncated = fields.flatMap(([name, field]) =>
|
|
field.cut ? [name] : [],
|
|
);
|
|
const entry: BuildComputerAction = {
|
|
index,
|
|
action,
|
|
tool,
|
|
arguments: Object.fromEntries(
|
|
fields.map(([name, field]) => [name, field.value]),
|
|
),
|
|
...(truncated.length === 0 ? {} : { truncated_arguments: truncated }),
|
|
started_at: new Date().toISOString(),
|
|
ended_at: null,
|
|
outcome: "pending",
|
|
};
|
|
this.#total += 1;
|
|
this.#keep(this.#kept.length, entry);
|
|
return {
|
|
index,
|
|
settle: (outcome) => {
|
|
if (outcome === "applied")
|
|
this.#lastApplied[action] = Math.max(
|
|
index,
|
|
this.#lastApplied[action] ?? index,
|
|
);
|
|
// An action dropped while pending leaves only its outcome above.
|
|
const slot = index - this.dropped;
|
|
if (this.#kept[slot] !== undefined)
|
|
this.#keep(slot, {
|
|
...entry,
|
|
outcome,
|
|
ended_at: new Date().toISOString(),
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
/** Put an entry in its slot, then drop the oldest until the rest fit the allowance. */
|
|
#keep(slot: number, entry: BuildComputerAction): void {
|
|
const bytes = jsonBytes(entry);
|
|
this.#bytes += bytes - (this.#kept[slot]?.bytes ?? 0);
|
|
this.#kept[slot] = { entry, bytes };
|
|
while (this.#kept.length > LOGGED_ACTIONS || this.#bytes > LOGGED_BYTES)
|
|
this.#bytes -= this.#kept.shift()?.bytes ?? 0;
|
|
}
|
|
}
|
|
|
|
/** What later stages read of a computer's action log. */
|
|
export type ReadonlyActionLog = Pick<
|
|
BuildComputerActionLog,
|
|
"total" | "dropped" | "entries" | "lastApplied"
|
|
>;
|