Skip to content
99 linesCodeBlameRaw
1/**
2 * The pure parts of an agent's computer (computer.ts): how a command's
3 * streamed lines become one transcript, how its end is said in a session's
4 * transcript, and how the container's NDJSON is read. No Workers imports,
5 * so Node runs the tests on the file as it is.
6 */
7import type { AgentComputerCommand } from "@g1t/contracts";
8
9/** How much of one command's output a kept transcript holds: `COMPUTER_TRANSCRIPT_BYTES` (@g1t/contracts runner.ts), which computer.ts passes. */
10const DEFAULT_TRANSCRIPT_BYTES = 64 * 1024;
11
12/** The object key a computer's saved home is kept under. */
13export function homeKey(agentId: string): string {
14 return `homes/${agentId}.tar.zst`;
15}
16
17/** A command's lines gathered into one transcript, in order, cut at the cap. */
18export function gather(lines: { stream: string; line: string }[], cap = DEFAULT_TRANSCRIPT_BYTES): { output: string; truncated: boolean } {
19 let output = "";
20 let truncated = false;
21 for (const entry of lines) {
22 const next = output ? `${output}\n${entry.line}` : entry.line;
23 if (next.length > cap) {
24 truncated = true;
25 break;
26 }
27 output = next;
28 }
29 return { output, truncated };
30}
31
32/** `outcome` for a session's transcript: `exit 0 · 1.2 s`, with what went wrong. */
33export function outcomeLine(command: Pick<AgentComputerCommand, "exit_code" | "duration_ms" | "timed_out" | "truncated">): string {
34 const seconds = command.duration_ms / 1000;
35 const took = seconds < 10 ? `${seconds.toFixed(1)} s` : seconds < 90 ? `${Math.round(seconds)} s` : `${Math.round(seconds / 60)} min`;
36 return [`exit ${command.exit_code}`, took, command.timed_out ? "timed out" : null, command.truncated ? "output cut" : null].filter(Boolean).join(" · ");
37}
38
39/** Byte parts as one array. */
40export function concat(parts: Uint8Array[], size: number): Uint8Array {
41 const out = new Uint8Array(size);
42 let at = 0;
43 for (const part of parts) {
44 out.set(part, at);
45 at += part.byteLength;
46 }
47 return out;
48}
49
50/** The lines of an NDJSON body as they arrive, each parsed; lines that aren't JSON objects are skipped. */
51export async function* ndjson(body: ReadableStream<Uint8Array>): AsyncGenerator<Record<string, unknown>> {
52 const reader = body.getReader();
53 const decoder = new TextDecoder();
54 let buffer = "";
55 const parse = (line: string): Record<string, unknown> | null => {
56 if (!line.trim()) return null;
57 try {
58 const value = JSON.parse(line) as unknown;
59 return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : null;
60 } catch {
61 return null;
62 }
63 };
64 for (;;) {
65 const { done, value } = await reader.read();
66 if (done) break;
67 buffer += decoder.decode(value, { stream: true });
68 let at: number;
69 while ((at = buffer.indexOf("\n")) >= 0) {
70 const parsed = parse(buffer.slice(0, at));
71 buffer = buffer.slice(at + 1);
72 if (parsed) yield parsed;
73 }
74 }
75 const last = parse(buffer + decoder.decode());
76 if (last) yield last;
77}
78
79/**
80 * What a command's streamed lines and closing line become, as kept: the
81 * output with the closing note on its last line, or a line saying the
82 * computer stopped answering when there was no closing line.
83 */
84export function transcriptOf(
85 lines: { stream: string; line: string }[],
86 closing: { exit_code?: number; duration_ms?: number; truncated?: boolean; timed_out?: boolean; note?: string } | null,
87 problem: string | null,
88 cap = DEFAULT_TRANSCRIPT_BYTES,
89): Pick<AgentComputerCommand, "output" | "truncated" | "timed_out" | "exit_code"> & { duration_ms: number | null } {
90 const { output, truncated } = gather(lines, cap);
91 const tail = closing ? (closing.note ? `[${closing.note}]` : null) : `[The computer stopped answering${problem ? `: ${problem}` : ""}.]`;
92 return {
93 output: tail ? `${output}${output ? "\n" : ""}${tail}` : output,
94 truncated: truncated || Boolean(closing?.truncated),
95 timed_out: Boolean(closing?.timed_out),
96 exit_code: typeof closing?.exit_code === "number" ? closing.exit_code : -1,
97 duration_ms: typeof closing?.duration_ms === "number" ? closing.duration_ms : null,
98 };
99}