Pi từ mã nguồnpi v1.4.0

Chương 20Phần 5: Chord và Pi Durable

pi-env, telemetry và evals

Chạy tool qua SSH với pi-env, hợp đồng telemetry có kiểu, và đo hành vi bằng evals.

  • 27 phút đọc
  • 28 đoạn code
  • 17 mục
Trong chương này17 mục
  1. pi-env: tool chạy ở máy khác, phần bền ở lại
  2. Mô hình tư duy: client áp luật của Node, daemon làm system call
  3. Triển khai qua SSH
  4. Giao thức: dòng đồng bộ, frame và hello
  5. Bên trong daemon: luồng, huỷ, liveness
  6. RemoteExecutionEnv: hiện thực ExecutionEnv
  7. Lệnh và cửa sổ output
  8. Watch, listFiles và terminal
  9. Lần theo một lệnh bash trên máy từ xa
  10. pi-telemetry: hợp đồng span, sự kiện, metric
  11. Schema telemetry có kiểu
  12. Pi Durable và telemetry
  13. pi-evals: đo hành vi của coding agent
  14. Quyết định thiết kế
  15. Cạm bẫy và trường hợp biên
  16. Đọc code ở đâu
  17. Tóm tắt

Chương này gom ba package đứng quanh lõi, mỗi cái trả lời một câu hỏi khác nhau. pi-env (@earendil-works/pi-env): làm sao để tool của một agent Pi Durable chạy trên máy khác, qua SSH, mà kết quả giống hệt khi chạy tại chỗ. pi-telemetry (@earendil-works/pi-telemetry): làm sao để các package của Pi mô tả những gì chúng làm (span, sự kiện (event), metric) mà không buộc vào một backend quan sát nào. pi-evals (packages/evals, không phát hành): làm sao đo hành vi của coding agent, đặc biệt là tài liệu của nó có giúp mô hình làm đúng việc hay không. Cách Pi Durable dùng một ExecutionEnv nằm ở chương Durable: task, scheduler, generation và tool; ở đây ta xem phần hiện thực từ xa.

pi-env: tool chạy ở máy khác, phần bền ở lại#

Bài toán: một worker Pi Durable chạy trên máy của bạn (hay trong một tiến trình server), giữ storage, khoá API và lịch sử hội thoại. Code cần sửa và lệnh cần chạy lại nằm trên một máy khác: một máy GPU, một điện thoại Android chạy Termux, một máy Windows. Mang cả worker sang đó nghĩa là mang theo credential và storage. pi-env tách hai thứ: "tool của agent chạy trên máy khác, thường qua SSH, còn worker Durable, storage và credential của nó ở lại máy cục bộ" (README.md:3–4).

README: hai nửa của pi-env
pi/packages/env/README.mddòng 6–10Markdown
- `pi-env` (`daemon/`): a small Rust program that runs on the remote machine. It speaks a framed protocol on stdin and  stdout ([docs/protocol.md](docs/protocol.md)) and performs file operations and commands there.- `RemoteExecutionEnv`: a Durable `ExecutionEnv` that talks to the daemon through a `Connection`. Its results match  `NodeExecutionEnv` running on the remote machine; only error messages may differ  ([docs/semantics.md](docs/semantics.md)).
Câu cần nhớ: kết quả của RemoteExecutionEnv khớp với NodeExecutionEnv chạy trên máy từ xa; chỉ thông báo lỗi được phép khác.

Package phụ thuộc Chord (chỉ để lấy kiểu Context) và pi-durable (để lấy giao diện ExecutionEnv và các kiểu kết quả) (package.json:47–50). Bản đầu tiên ra ở 1.0.4 (2026-10-05) với RemoteExecutionEnv, Connection và phần khởi động qua SSH; 1.2.0 thêm listFiles, LocalExecutionEnv và pseudo-terminal (CHANGELOG.md:7–24). Giao diện nó hiện thực thuộc Pi Durable, vốn được ghi là thử nghiệm, nên pi-env cũng di chuyển theo.

Mô hình tư duy: client áp luật của Node, daemon làm system call#

 Máy cục bộ                                             Máy từ xa
 Harness · task pi.tool · tool bash/read/edit/write
     │ ExecutionEnv (FileSystem + Shell)
 RemoteExecutionEnv   giải đường dẫn, điểm kiểm abort, map mã lỗi,
     │                cỡ lần đọc của readFile, luật kết quả của exec
     │ request(op, json, payload) · cancel · kill
 Connection ── spawn: ssh -T … -- host '~/.pi/mobile/tools/pi-env-<sha>' serve --token <hex>
     │   stdin:  frame request · cancel · ping           ──────▶  pi-env (Rust)
     │   stdout: "PI-ENV <token>\n", rồi chỉ frame      ◀──────  system call · tiến trình · spill
     │                                                             watch (cổng của NodeFileWatcher)
 storage · credential · gọi mô hình: ở lại                    listFiles · pty
Một Connection quản đúng một tiến trình daemon tại một thời điểm; daemon được khởi động lười ở request đầu và khởi động lại sau khi mất. ExecutionEnv của Durable được định nghĩa ở durable/src/env/index.ts:374.

Chuẩn đối chiếu là NodeExecutionEnv của Durable chạy trên máy từ xa; bộ kiểm tuân thủ (conformance) cho env của Durable và một test vi sai (differential) kiểm từng luật (semantics.md:3–4). Để đạt điều đó, việc được chia rõ:

semantics.md: phần nào ở client, phần nào ở daemon
pi/packages/env/docs/semantics.mddòng 6–19Markdown
## Split of work| Concern | Where ||---|---|| Path resolution: `~` (remote home), `file://`, relative to `cwd`; on Windows drive-relative paths against the remote's working directories | client, the remote system's `path` rules || Abort checkpoints of each method | client, as `NodeExecutionEnv` || Error code mapping (`ENOENT` → `not_found`, ...) | client, as `NodeExecutionEnv`'s `toFileError` || `readFile`'s read sizes, size limit and decoding (`StringDecoder`, byte-order mark kept) over the daemon's `open(path, "r")` | client || `writeFile` in 512 KiB writes with an abort check before each; `appendFile` without checks | client, over one open file || Line reader | client, positional reads and Durable's `StreamDecoder` || Watching: snapshots, native events, polling | daemon, a port of Durable's `NodeFileWatcher` || Timeout validation, result precedence (callback error, timeout, abort, spill failure, exit code) | client || System calls, processes, spill files, line scans | daemon || Command output decoding: per stream, WHATWG UTF-8, only a leading byte-order mark dropped | daemon (`encoding_rs`) |
Client giữ mọi luật mà Node áp trước và sau system call (đường dẫn, điểm kiểm abort, map mã lỗi, cách readFile chia lần đọc); daemon làm system call, quản tiến trình, file tràn (spill: toàn bộ output được ghi ra một file tạm khi vượt ngưỡng), quét dòng, và chạy watcher ngay cạnh file.

Lý do cách chia này cần thiết chứ không phải tuỳ chọn: nếu daemon tự diễn giải đường dẫn hay tự quyết khi nào kiểm abort, nó sẽ phải sao chép lại hành vi của Node bằng Rust, và mỗi khác biệt nhỏ là một kết quả tool khác đi. Giữ luật ở client, chạy đúng code TypeScript đó, thì chỉ còn system call là phải khớp, và system call được kiểm bằng conformance.

Triển khai qua SSH#

Mọi lần gọi ssh đi qua cùng một bộ tham số, chặt hơn mặc định ở mọi chỗ có thể:

Tin một host key#

Vì StrictHostKeyChecking=yes, máy lạ bị từ chối với HostKeyUnknownError. scanHostKey(target) kết nối một lần với accept-new vào một file known-hosts tạm, qua đúng đường mà kết nối thật sẽ đi, rồi trả về các dòng key và fingerprint (qua ssh-keygen -lf); comment ghi rõ hiển thị fingerprint không phải là xác thực, chủ máy phải so với một bản lấy qua kênh khác (ssh.ts:177–207). acceptHostKey chỉ nhận dòng thuần cho đúng alias, và từ chối với HostKeyChangedError nếu key cùng loại đã có mà khác; muốn đổi phải forgetHostKey trước (ssh.ts:253–286). File known-hosts được ghi lại nguyên tử, mỗi file một thay đổi tại một thời điểm (ssh.ts:224–251).

Phát hiện hệ thống và triển khai daemon#

detectPlatform() chạy một probe POSIX (uname -s, uname -m, uname -o, $HOME, $TMPDIR, $LD_PRELOAD) qua shell đăng nhập; không được thì thử PowerShell; gặp Git Bash làm shell mặc định của SSH trên Windows thì hỏi lại PowerShell; với Windows, echo %OS% phân biệt cmd.exe với PowerShell. Termux nhận thêm cảnh báo: thiếu TMPDIR, thiếu termux-exec, Android có thể tạm dừng Termux (ssh.ts:288–353). Package chở sẵn daemon cho mọi hệ được hỗ trợ trong bin/pi-env-<platform>-<arch>/ (ssh.ts:355–359).

Kết nối thật là một Connection mà lệnh khởi động là một hàm, chạy lại ở mỗi lần khởi động: phát hiện hệ thống một lần, kiểm (và nếu cần triển khai) daemon, rồi trả về dòng lệnh ssh:

sshConnectionFor(): mỗi lần khởi động daemon đều kiểm lại binary
pi/packages/env/src/ssh.tsdòng 489–511TypeScript
/** A `Connection` whose every start detects the remote system (once), verifies or deploys the daemon, then starts it. */function sshConnectionFor(options: SshConnectOptions, state: SshState): Connection {	return new Connection({		command: async () => {			if (state.remote === undefined) {				const remote = await detectPlatform(options);				for (const warning of remote.warnings) options.onLog?.(`${warning}\n`);				state.remote = remote;			}			// A deployment that just verified the binary counts for the first start.			const file =				state.verified ??				(await deployDaemon(options, state.remote, options.binary ?? packagedDaemon(state.remote)));			state.verified = undefined;			return [				options.ssh ?? "ssh",				...sshArguments(options),				launchCommand(state.remote, file, options.loginShell === true),			];		},		...(options.onLog === undefined ? {} : { onLog: options.onLog }),	});}
Một lần triển khai vừa kiểm binary được tính cho lần khởi động đầu (state.verified), nên connectSsh không kiểm hai lần. Từ lần khởi động thứ hai, kiểm hash là một vòng SSH thêm trước khi chạy daemon.

Hai cách lấy kết nối. connectSsh(target) phát hiện và triển khai ngay, và ném lỗi tại chỗ (ví dụ HostKeyUnknownError). sshConnection(target) không làm gì cho tới request đầu tiên; lỗi (mất mạng, host key chưa tin) thành lỗi spawn_error của request đó, và request sau thử lại (ssh.ts:513–539). Cách lười quan trọng với Durable: tạo một môi trường (trong HarnessOptions.env) không bao giờ cần mạng.

Giao thức: dòng đồng bộ, frame và hello#

Client chạy pi-env serve --token <hex> và nói chuyện qua stdin/stdout của tiến trình đó; stderr chỉ là chẩn đoán cho log (protocol.md:3–4). Vấn đề đầu tiên là file khởi động của shell: .bashrc của máy từ xa có thể in một banner lên stdout trước khi daemon chạy. Token giải quyết việc đó: dòng đầu tiên daemon in ra là PI-ENV <token>\n với token lấy từ dòng lệnh (daemon/src/main.rs:472–477), và client bỏ mọi thứ trước dòng đó:

Connection.#onData(): tìm dòng đồng bộ, chuyển phần nhiễu sang log
pi/packages/env/src/connection.tsdòng 335–355TypeScript
#onData(session: Session, chunk: Buffer): void {	if (!session.live) return;	session.lastSeen = Date.now();	session.chunks.push(chunk);	session.buffered += chunk.length;	if (!session.synced) {		// Shell startup files may print before the daemon runs; skip everything before its sync line.		const marker = Buffer.from(`PI-ENV ${session.token}\n`, "utf8");		const all = session.peek(session.buffered);		const index = all.indexOf(marker);		if (index === -1) {			if (session.buffered > 1024 * 1024) {				session.consume(session.buffered - marker.length);			}			return;		}		const noise = all.subarray(0, index).toString("utf8").trim();		if (noise !== "") this.#options.onLog?.(noise);		session.consume(index + marker.length);		session.synced = true;	}
Token là 16 byte ngẫu nhiên, tạo mới cho mỗi lần khởi động (connection.ts:280–281). Nhiễu quá 1 MiB thì phần cũ bị bỏ, chỉ giữ đủ để còn tìm được dòng đồng bộ.

Sau dòng đó, stdout chỉ còn frame. Mọi frame, cả hai chiều, có cùng khung: u32 length, u8 type, u32 id, u32 jsonLength, JSON, rồi payload thô; số nguyên big-endian, frame tối đa 16 MiB (protocol.md:12–27). Phía client:

frame(): JSON và payload thô trong một khung có độ dài
pi/packages/env/src/connection.tsdòng 105–118TypeScript
/** Lone surrogates become U+FFFD, as Node encodes strings for system calls. */function wellFormed(_key: string, value: unknown): unknown {	return typeof value === "string" ? value.toWellFormed() : value;}function frame(type: number, id: number, json: Json, payload: Uint8Array = new Uint8Array(0)): Buffer {	const body = Buffer.from(JSON.stringify(json, wellFormed), "utf8");	const header = Buffer.alloc(13);	header.writeUInt32BE(9 + body.length + payload.length, 0);	header.writeUInt8(type, 4);	header.writeUInt32BE(id, 5);	header.writeUInt32BE(body.length, 9);	return Buffer.concat([header, body, payload]);}
toWellFormed() đổi surrogate lẻ thành U+FFFD trước khi mã hoá UTF-8, đúng cách Node mã hoá chuỗi đường dẫn và tham số cho system call. Nội dung file và output đi dạng bytes thô trong payload, không bao giờ qua JSON.

Phía daemon, read_frame kiểm độ dài; JSON không parse được vẫn giữ khung nguyên vẹn (frame tới với Value::Null và được trả lời lỗi EINVAL), còn lỗi khung thì kết thúc kết nối (daemon/src/frame.rs:49–82). Sáu loại frame:

typeTênChiềuNghĩa
1requestclient → daemonjson.op là tên thao tác
2resultdaemon → clientRequest id thành công và kết thúc
3errordaemon → clientRequest id thất bại và kết thúc: { code, message, syscall?, path? }
4eventdaemon → clientTiến độ của request đang chạy (output của exec, pty; thay đổi của watch)
5cancelclient → daemonHuỷ request id; { mode: "kill" } là giết mà không huỷ
6pingcả haiCòn sống; bên nhận bỏ qua

Bảng gốc ở protocol.md:28–39. code là mã kiểu Node (ENOENT, EISDIR, ERR_FS_EISDIR...) hoặc một mã thực thi (shell_unavailable, spawn_error, timeout, aborted, unknown); chỉ mã là hợp đồng, thông báo là chẩn đoán. Đường dẫn luôn tuyệt đối: client giải ~, đường dẫn tương đối và URL file:// trước khi gửi.

Request đầu tiên là hello { protocol }; daemon trả về phiên bản giao thức và package, hệ điều hành và kiến trúc theo cách viết của Rust (linux, macos, x86_64, aarch64), thư mục home, tmpdir theo luật os.tmpdir() của Node, thư mục làm việc (kể cả thư mục làm việc theo ổ của Windows), pid, và features: các thao tác thêm vào giao thức 1 sau bản đầu (hiện chỉ listFiles) (main.rs:148–160; protocol.md:71–77). Client từ chối mọi phiên bản khác 1 (connection.ts:309–311), và nodePlatform(info) đổi tên sang cách viết của Node (win32, darwin, x64, arm64) (connection.ts:55–64).

Bên trong daemon: luồng, huỷ, liveness#

Daemon là một chương trình Rust nhỏ, phụ thuộc encoding_rs, ignore (crate của fd và ripgrep), notify, serde_json và libc hoặc windows-sys (daemon/Cargo.toml:8–25). Một luồng đọc frame từ stdin và phân phối:

Một Control được đăng ký cho mỗi request trước khi nó chạy, nên một cancel gửi ngay sau request không bao giờ mất (protocol.md:45–51; exec.rs:39–58). Huỷ exec hay pty giết process group của lệnh và kết thúc với aborted; scanLines dừng giữa các khúc, listFiles giữa các mục; watch dừng và trả {}.

Trả lời đi trước output#

Output: một người ghi stdout, frame control luôn đi trước output số lượng lớn
pi/packages/env/daemon/src/output.rsdòng 1–4, 71–88Rust
//! The single writer of stdout. Control frames (results, errors, pings, watch events) always go before bulk command//! output, so a slow link delays output but never a cancel's result or a ping. Bulk output is bounded: readers of//! commands without an output window wait while too much of it is unsent, which slows the command down like a full//! pipe would.    pub fn run(&self, writer: &mut impl Write) {        loop {            let (frame, unsent) = {                let mut queues = self.queues.lock().unwrap();                loop {                    if let Some(frame) = queues.control.pop_front() {                        break (frame, None);                    }                    if let Some((frame, unsent)) = queues.bulk.pop_front() {                        queues.bulk_bytes -= frame.payload.len();                        break (frame, unsent);                    }                    if queues.closed {                        return;                    }                    queues = self.queued.wait(queues).unwrap();                }            };
Kết quả của một lệnh đi sau output của nó; kết quả có payload trên 64 KiB cũng xếp hàng sau output, để một link chậm làm trễ dữ liệu lớn chứ không trễ câu trả lời nhỏ hay ping (main.rs:362–375). Output của lệnh không có cửa sổ chờ khi hơn 4 MiB chưa gửi, làm chậm lệnh như một pipe đầy.

Liveness: không để lại tiến trình mồ côi#

Mỗi bên gửi ping mỗi 5 giây. Daemon giết mọi process group nó đã khởi động và thoát sau 30 giây không nhận được byte nào, hoặc khi stdin kết thúc (main.rs:494–520). Client tính ngược lại: 30 giây im lặng là kết nối đã mất, mọi request đang chờ bị từ chối với { code: "unknown", lost: true }, và request sau khởi động một daemon mới (connection.ts:301–307; protocol.md:62–67). Hệ quả có chủ đích: một điện thoại mất mạng giữa chừng không để lại lệnh nào chạy trên máy từ xa. Với lệnh có hiệu ứng, kết quả lúc đó là không biết: comment của Connection nói thẳng "for mutations their outcome is then unknown" (connection.ts:177–180).

RemoteExecutionEnv: hiện thực ExecutionEnv#

ExecutionEnv của Durable là FileSystem cộng Shell: khoảng hai mươi phương thức file (đọc, ghi, reader nhị phân và theo dòng, đọc thư mục, watch...) và exec cộng cleanup; mọi phương thức trả về Result chứ không ném lỗi, và nhận Context làm tham số cuối (durable/env/index.ts:209–284; chương task giải thích từng phương thức). RemoteExecutionEnv hiện thực đúng giao diện đó; cấu hình gồm connection, id (không gian tên file: hai môi trường cùng id thấy cùng file, ví dụ pi-env:gpu), cwd, và tuỳ chọn shellPath, shellEnv, watch (remote-env.ts:51–61).

Đường dẫn được giải ở client, theo luật của máy từ xa#

#resolve(): resolvePath của Node, nhưng với home, dấu phân cách và thư mục làm việc của máy từ xa
pi/packages/env/src/remote-env.tsdòng 373–402TypeScript
/** Node's `resolvePath` on the remote system, with its home directory and working directories. */async #resolve(path: string): Promise<string> {	const info = await this.connection.info();	const windows = nodePlatform(info).platform === "win32";	const paths = windows ? win32 : posix;	let normalized = path;	if (normalized === "~") {		normalized = info.home;	} else if (normalized.startsWith("~/") || (windows && normalized.startsWith("~\\"))) {		normalized = paths.join(info.home, normalized.slice(2));	} else if (normalized.startsWith("file://")) {		try {			normalized = fileURLToPath(normalized, { windows });		} catch {			// Keep malformed URLs as ordinary paths, as Node does.		}	}	if (paths.isAbsolute(normalized)) return paths.resolve(normalized);	if (windows) {		// A drive-relative path on another drive: Node on Windows resolves it against that drive's working directory		// (`=D:`), else its own working directory if on that drive, else the drive's root.		const drive = /^([a-zA-Z]:)(?![\\/])/.exec(normalized)?.[1];		if (drive !== undefined && drive.toLowerCase() !== this.cwd.slice(0, 2).toLowerCase()) {			let base = info.driveCwds[drive.toUpperCase()] ?? info.cwd;			if (base.slice(0, 2).toLowerCase() !== drive.toLowerCase() && base[2] === "\\") base = `${drive}\\`;			return win32.resolve(base, normalized);		}	}	return paths.resolve(this.cwd, normalized);}
path.win32 hay path.posix được chọn theo hello, nên một worker trên macOS giải đúng D:foo của một máy Windows: theo thư mục làm việc của ổ D: (biến =D:), nếu không có thì theo thư mục làm việc của daemon nếu cùng ổ, nếu không thì gốc ổ.

Mọi thao tác file đi qua #fileOp: giải đường dẫn, kiểm abort trước, chạy, và với vài thao tác kiểm abort cả sau, đúng các điểm kiểm của NodeExecutionEnv; lỗi được map về FileError (remote-env.ts:404–428). Bảng map mã lỗi là bảng của toFileError trong Node env: ENOENT thành not_found, EACCES/EPERM thành permission_denied, ENOTDIR, EISDIR, EINVAL... còn lại là unknown (errors.ts:8–34).

Handle chết theo daemon#

Reader nhị phân, reader theo dòng và reader thư mục giữ một handle số do daemon cấp. Handle chỉ có nghĩa với tiến trình daemon đã mở nó; sau khi kết nối mất và một daemon mới được khởi động, gửi handle cũ tới daemon mới sẽ đọc nhầm file khác. Connection chặn điều đó bằng id của session:

request(): request gắn với một session thì thất bại thay vì tới một daemon mới
pi/packages/env/src/connection.tsdòng 203–213TypeScript
async request(op: string, json: Json, options: RequestOptions = {}): Promise<Reply> {	if (options.session !== undefined) {		const current = this.#session;		if (current === undefined || current.id !== options.session || !current.live) {			throw lost("pi-env connection lost");		}		return this.#send(current, op, json, options);	}	const { session } = await this.#connect();	return this.#send(session, op, json, options);}
Mỗi Handle nhớ session của câu trả lời đã cấp nó và gửi mọi request của mình kèm session đó (remote-env.ts:133–158). Daemon giới hạn 4.096 handle mở mỗi kết nối, quá thì EMFILE (main.rs:42–43).

Đọc và ghi theo khúc, nhiều khúc cùng bay#

Một vòng request qua SSH tốn thời gian, nên đọc tuần tự từng khúc sẽ chậm. readRange() giữ tới 8 lệnh pread cùng bay; một lần đọc ngắn (chạm cuối file) huỷ các giả định của những lần đang bay và đọc tiếp ngay sau nó, đúng như đọc tuần tự (remote-env.ts:94–131). readTextFile/readBinaryFile mở file như open(path, "r") của Node, rồi theo đúng cách readFile của Node chia lần đọc: kích thước 0 (FIFO, thiết bị) thì đọc tuần tự 64 KiB tới hết, không giải mã thì một lần cho file nhỏ hay các khúc 512 KiB, giải mã UTF-8 thì bằng StringDecoder (remote-env.ts:451–524). Ghi đi chiều ngược lại:

Một chi tiết nhỏ cho thấy mức độ đòi hỏi "khớp với Node": listDir sắp tên theo bytes trên POSIX như scandir của libuv, dùng bytes gốc (raw) của tên không phải UTF-8; trên Windows thì giữ thứ tự của file system như daemon báo (remote-env.ts:704–710). Các luật mà thư viện chuẩn không tự làm (mkdir đệ quy, rm, mkdtemp, tmpdir, /dev/null, FIFO...) được liệt kê ở semantics.md:21–42.

Lệnh và cửa sổ output#

cleanup() dành cho lúc chủ môi trường tắt: nó gửi cancel với mode: "kill" cho mọi lệnh môi trường này còn chạy, giết mà không huỷ, nên lệnh kết thúc với trạng thái của tiến trình bị giết (137), đúng như NodeExecutionEnv (remote-env.ts:950–954; connection.ts:221–225). Phía daemon, ngữ nghĩa lệnh theo semantics.md:44–60: chuỗi chạy qua /bin/bash, which bash, rồi sh với -c, mảng argv chạy thẳng; thư mục làm việc phải tồn tại; môi trường là của daemon, rồi shellEnv, rồi env. Mỗi lệnh có một session (process group) mới với signal mặc định và mask rỗng, nên timeout, abort và cleanup() giết cả cây bằng SIGKILL:

sys/unix.rs: setsid trước exec, giết cả process group, 128 + signal như shell
pi/packages/env/daemon/src/sys/unix.rsdòng 266–304Rust
    // SAFETY: only async-signal-safe calls between fork and exec.    unsafe {        command.pre_exec(|| {            // A new session, so a kill reaches every descendant; default signal handling and an empty mask, as libuv sets.            libc::setsid();            for signal in 1..32 {                if signal != libc::SIGKILL && signal != libc::SIGSTOP {                    libc::signal(signal, libc::SIG_DFL);                }            }            let mut mask: libc::sigset_t = std::mem::zeroed();            libc::sigemptyset(&mut mask);            libc::sigprocmask(libc::SIG_SETMASK, &mask, std::ptr::null_mut());            Ok(())        });    }}pub fn after_spawn(_child: &Child) {}/// Kill the command's process group, or the process if the group is gone.pub fn kill_tree(pid: u32) {    let pid = pid as i32;    // SAFETY: plain signal delivery.    unsafe {        if libc::kill(-pid, libc::SIGKILL) != 0 {            libc::kill(pid, libc::SIGKILL);        }    }}/// A process killed by a signal has no exit code; report 128 + the signal, as the shell does.pub fn exit_code(status: ExitStatus) -> i64 {    i64::from(        status            .code()            .unwrap_or_else(|| 128 + status.signal().unwrap_or(0)),    )}

Sau khi tiến trình thoát, output vẫn được gom cho tới khi cả hai pipe đóng, hoặc 100 ms không có output: một tiến trình con giữ pipe mở không làm lệnh treo mãi (exec.rs:382–385).

Cửa sổ output: không chuyển những gì sẽ bị bỏ#

Bài toán: yes | head -c 2G trên máy từ xa sinh hai gigabyte output, trong khi tool bash của Durable chỉ giữ phần đuôi (mặc định 2.000 dòng hoặc 50 KiB cuối, durable/harness/tool.ts:294–296; bash.ts:151) và commit tiến độ mỗi 100 ms. Gửi cả hai gigabyte qua SSH để rồi vứt gần hết là lãng phí, và nó làm nghẽn mọi thứ khác trên link. Giao diện của Durable có sẵn chỗ cho điều này:

ShellExecOptions.window: phần đuôi người gọi giữ, và nhịp nó lấy mẫu
pi/packages/durable/src/env/index.tsdòng 310–331TypeScript
	spill?: ShellSpillOptions;	/**	 * The caller keeps only this tail of the output, so the environment may omit output outside it and report the omission	 * as `info.skipped`. Without it, every chunk is delivered.	 */	window?: ShellOutputWindow;}/** * The tail of the combined output a caller keeps, and how often it samples it. An environment that transfers output * over a slow link uses it to omit what the caller would drop anyway and to send no faster than the caller commits. */export interface ShellOutputWindow {	/** UTF-8 bytes of decoded text kept at the end of the output. */	maxBytes: number;	/** Lines kept at the end of the output. */	maxLines: number;	/** Minimum pause between the caller's samples of the output. */	minIntervalMs: number;	/** Each sample also pauses the caller in proportion to its size at this rate. */	bytesPerSecond: number;}
Đổi lại, môi trường báo phần đã bỏ qua ShellOutputInfo.skipped (bytes, newlines, endsWithNewline), và chunk đi sau một lần bỏ luôn chứa nhiều hơn cửa sổ ít nhất một byte hoặc một dòng, nên phần bị bỏ chắc chắn không thể nằm trong phần đuôi được giữ (durable/env/index.ts:347–357).

Durable dựng cửa sổ từ giới hạn output của tool (chỉ khi tool giữ phần đuôi) và nhịp commit tiến độ của nó (durable/harness/tool.ts:333–341), tool bash chuyển nó vào exec (durable/tools/bash.ts:104–107), và RemoteExecutionEnv gửi nguyên nó tới daemon. Ở đó, window.rs giữ output chưa giao, gộp nó lại, và thay phần chắc chắn nằm ngoài cửa sổ bằng số đếm:

window.rs: cắt tiền tố dài nhất mà phần còn lại vẫn chứng minh nó nằm ngoài cửa sổ; nhịp giao theo nhịp commit của người gọi
pi/packages/env/daemon/src/window.rsdòng 1–3, 97–104, 166–176Rust
//! Durable's output window (`ShellExecOptions.window`, `ShellOutputInfo.skipped`): undelivered command output is held//! and coalesced; text followed by more than the window (by a byte or a line) can never be in the caller's tail and is//! replaced by counts. Delivery follows the caller's commit pace, and waits while an earlier frame is still unsent.    /// Whether the text after a cut still proves the cut text is outside the window.    fn proves(&self, bytes: usize, newlines: usize) -> bool {        bytes > self.window.max_bytes || newlines > self.window.max_lines    }    /// Replace the longest prefix whose remainder still proves exclusion by counts.    fn trim(&mut self) {        let mut removed = String::new();    /// Deliver everything held: one event carrying the skip and all text after it, or one event per segment.    pub fn take(&mut self) -> Vec<Event> {        let segments = std::mem::take(&mut self.segments);        let delivered = self.bytes;        self.bytes = 0;        self.newlines = 0;        // The caller writes at most the window per sample, and paces itself by what it wrote.        let pace = Duration::from_secs_f64(            delivered.min(self.window.max_bytes) as f64 / self.window.bytes_per_second,        );        self.next_send = Instant::now() + self.window.min_interval.max(pace);
Một lần giao gửi một event mang skipped cùng toàn bộ text sau nó, rồi hẹn lần giao kế sau max(minIntervalMs, phần đã giao / bytesPerSecond). Vòng lặp của lệnh chỉ giao khi tới hạn và frame trước đã được ghi ra stdout, nên tối đa một frame output của mỗi lệnh đang chờ (exec.rs:386–392).

Phía client, event.skipped thành info.skipped của onOutput, tool bash gọi api.output(text, info.skipped), và OutputBuffer.push(chunk, skipped) của Chord cộng số đếm vào tổng và bỏ phần đã lưu trước đó, vì không gì đứng trước chỗ bị bỏ còn có thể nằm trong cửa sổ (chord/src/output.ts:154–182). Kết quả: phần đuôi giữ lại và số byte, số dòng bị bỏ y hệt khi chạy tại chỗ, nhưng chỉ phần đuôi đi qua mạng. Đây là ý của dòng "honors output windows so dropped command output is not transferred" trong changelog bản đầu (CHANGELOG.md:23).

Watch, listFiles và terminal#

Watch chạy cạnh file#

Daemon chạy một bản Rust của NodeFileWatcher của Durable ngay cạnh file, nên một lần quét không tốn vòng request nào: sự kiện gốc (inotify, FSEvents) chỉ kích một lần quét lại có debounce, và thay đổi là hiệu giữa hai ảnh chụp cộng các đường dẫn của sự kiện (daemon/src/watch.rs:1–3). Windows và file system mạng hay FUSE thì poll; hết watch gốc thì chuyển sang poll và báo overflow (semantics.md:62–68). Request watch kéo dài tới khi bị huỷ: event ready khi đã phủ xong, rồi change với paths hoặc overflow. Phía client, RemoteWatcher lo phần mất kết nối:

listFiles và LocalExecutionEnv#

listFiles là phương thức tuỳ chọn của ExecutionEnv: liệt kê cả cây dưới một thư mục như fd --hidden --exclude .git mà không theo symlink, bỏ những gì git ignore bên trong work tree, có giới hạn maxEntries (mặc định 50.000), maxBytes (4 MiB) và timeoutMs (10 s) với cờ truncated (protocol.md:101–112). Daemon dùng crate ignore của fd và ripgrep (daemon/src/list.rs:1–2), và trả kết quả dạng bytes chứ không phải JSON:

listFilesOver(): kiểm features trước, payload là các bản ghi kind byte + đường dẫn + NUL
pi/packages/env/src/list-files.tsdòng 36–71TypeScript
const aborted = abortResult<FileListing>(context.abortSignal, path);if (aborted) return aborted;try {	if (!(await connection.info()).features?.includes("listFiles")) {		return err(new FileError("not_supported", "The pi-env daemon does not list files; update it", path));	}	const { json, payload } = await connection.request(		"listFiles",		// Fields left undefined are not sent; the daemon applies the defaults.		{			path,			hidden: options?.hidden,			gitignore: options?.gitignore,			exclude: options?.exclude,			maxEntries: options?.maxEntries,			maxBytes: options?.maxBytes,			timeoutMs: options?.timeoutMs,		},		context.abortSignal ? { signal: context.abortSignal } : {},	);	const afterAbort = abortResult<FileListing>(context.abortSignal, path);	if (afterAbort) return afterAbort;	// Records of a kind byte, the UTF-8 path, and a NUL.	const entries: FileListEntry[] = [];	const bytes = Buffer.from(payload.buffer, payload.byteOffset, payload.length);	for (let start = 0; start < bytes.length; ) {		const end = bytes.indexOf(0, start);		if (end === -1) break;		const kind = KINDS[bytes[start]!];		if (kind !== undefined) entries.push({ path: bytes.toString("utf8", start + 1, end), kind });		start = end + 1;	}	return ok({ entries, truncated: json.truncated === true });} catch (error) {	return err(toFileError(error, path));}
Daemon phát hành trước listFiles không có tên đó trong hello.features, nên client trả not_supported mà không gửi request, và người gọi tự duyệt cây (durable/README.md:456–460).

Vì NodeExecutionEnv không có listFiles, pi-env thêm LocalExecutionEnv: NodeExecutionEnv cộng listFiles qua một daemon trên chính máy này (new Connection({ command: [localDaemon()] })), để môi trường cục bộ và từ xa liệt kê bằng cùng một code (list-files.ts:79–102). Connection thuộc về người gọi, và người gọi đóng nó.

Terminal#

openTerminal(connection, options) chạy một chương trình (mặc định shell đăng nhập của user) trong một pseudo-terminal của daemon, dẫn đầu một session mới với terminal làm terminal điều khiển; output là bytes thô của terminal, không giải mã như exec (protocol.md:134–151). RemoteTerminal có write() (chia input thành các khúc 1 MiB vì frame tối đa 16 MiB), resize() (chương trình nhận SIGWINCH), close() (giết cả session), và foreground() trả về process group đang ở tiền cảnh khi nó không phải chính chương trình, như một trình soạn thảo mà shell đang chạy (terminal.ts:26–90). ptyWrite, ptyResize, ptyForeground đánh địa chỉ request pty đang chạy bằng id của nó, và được trả lời theo thứ tự đến (đã thấy ở mục daemon). Terminal chưa hỗ trợ Windows (ENOTSUP).

Lần theo một lệnh bash trên máy từ xa#

Kịch bản: một Harness của Pi Durable chạy trên laptop, môi trường là new RemoteExecutionEnv({ connection: sshConnection(target).connection, id: "pi-env:gpu", cwd: "/home/me/project" }) như ví dụ của README (README.md:25–28), host key của gpu-box đã được tin, và đây là lệnh đầu tiên của tiến trình. Mô hình gọi tool bash với { "command": "pytest -q" } (lệnh là giả định). Các bước theo code, không phải in ra từ một lần chạy.

  1. 1. Tool bashTask pi.tool của Durable chạy execute của tool bash, vốn gọi env.exec(command, { cwd, env, inheritEnv, timeout, onOutput, spill, window }, context); window là đuôi 2.000 dòng / 50 KiB và nhịp 100 ms (bash.ts:95–110).
  2. 2. Kiểm ở clientRemoteExecutionEnv.exec kiểm timeout, giải cwd theo luật của máy từ xa, dựng biến môi trường (shellEnv rồi env khi kế thừa), rồi gọi connection.request("exec", …) với onStart (để cleanup() biết lệnh này) và onEvent.
  3. 3. Khởi động lườiChưa có session, nên #start() gọi hàm lệnh của sshConnectionFor: detectPlatform() (một vòng SSH), deployDaemon() kiểm hash ở xa và nếu thiếu thì tải lên, rồi trả về ["ssh", …sshArguments(target), "'/home/me/.pi/mobile/tools/pi-env-<32 hex>'"]. Connection spawn nó với serve --token <hex> (connection.ts:268–283).
  4. 4. Đồng bộ và hello.bashrc ở xa in một dòng chào; client chuyển nó vào onLog khi thấy PI-ENV <token>. Request đầu là hello { protocol: 1 }; thông tin máy (home, tmpdir, hệ điều hành) được nhớ cho mọi lần giải đường dẫn sau.
  5. 5. Request exec#send cấp id, đăng ký pending, gắn listener huỷ (abort của Context thành một frame cancel), rồi ghi frame(REQUEST, id, { …json, op: "exec" }) vào stdin của ssh (connection.ts:240–262).
  6. 6. Daemon chạy lệnhserve_frames đăng ký Control, cho exec một luồng riêng; exec::run chọn shell (/bin/bash -c "pytest -q"), kiểm cwd tồn tại, spawn với setsid, ghi pid vào tập process group, và mở hai luồng đọc pipe (exec.rs:242–320).
  7. 7. Output qua cửa sổBytes từ pipe được giải mã UTF-8 theo từng luồng, đẩy vào Pending; phần chắc chắn nằm ngoài đuôi được thay bằng số đếm. Tới hạn và frame trước đã ra stdout thì một event { kind: "output", stream, skipped? } mang text làm payload được xếp vào hàng bulk.
  8. 8. Về tới clientNgười ghi stdout của daemon gửi frame control trước, bulk sau; #onData tách frame, #dispatch gọi onEvent, RemoteExecutionEnv gọi onOutput(text, context, { stream, skipped }), tool bash gọi api.output(text, skipped), và OutputBuffer của Chord giữ đuôi; Progress quyết định khi nào commit phần đã giữ vào storage (chương task).
  9. 9. Kết quảTiến trình thoát; daemon gom output tới khi hai pipe đóng hoặc 100 ms im lặng, giao phần còn giữ, rồi gửi result { exitCode } qua hàng bulk, nên nó tới sau output của chính lệnh đó. exec trả ok({ exitCode }); tool dựng kết quả từ output đã giữ (bash.ts:126–135).
Ví dụ minh hoạ: các frame trên stdin/stdout của ssh (id và kích thước là giả định; ping bị lược)

 stdout  "Welcome to gpu-box\n"                                    → onLog, bỏ qua
 stdout  "PI-ENV 3f9c…e1\n"                                        → synced
 stdin   [len][1][id 1][jsonLen]{"protocol":1,"op":"hello"}
 stdout  [len][2][id 1][jsonLen]{"protocol":1,"version":"1.4.0","os":"linux","arch":"x86_64",
                                  "home":"/home/me","tmpdir":"/tmp",…,"features":["listFiles"]}
 stdin   [len][1][id 2][jsonLen]{"command":"pytest -q","cwd":"/home/me/project","env":{},
                                  "inheritEnv":true,"spill":{…},"window":{"maxBytes":51200,
                                  "maxLines":2000,"minIntervalMs":100,"bytesPerSecond":102400},"op":"exec"}
 stdout  [len][4][id 2][jsonLen]{"kind":"output","stream":"stdout"}  payload: "....F.\n"
 stdout  [len][4][id 2][jsonLen]{"kind":"output","stream":"stdout"}  payload: "1 failed, 5 passed\n"
 stdout  [len][2][id 2][jsonLen]{"exitCode":1}                      → kết quả lỗi exit_code của tool
Khung frame theo frame(): u32 độ dài, u8 loại, u32 id, u32 độ dài JSON, JSON, payload. Thứ tự khoá JSON là thứ tự JSON.stringify của { ...json, op }.

Nếu Wi-Fi rớt giữa chừng: sau 30 giây không nhận được byte nào, daemon giết process group của pytest và thoát; client, cũng sau 30 giây im lặng, huỷ session và từ chối request exec với { code: "unknown", lost: true }. exec trả err(ExecutionError("unknown", …)), tool bash ném lại lỗi đó, và Durable biến nó thành một kết quả lỗi vẫn mang output đã giữ (bash.ts:138–143). Lệnh tiếp theo khởi động một daemon mới (kiểm lại hash, không tải lại nếu đã có). Nếu chính tiến trình Durable chết thì lại là chuyện khác: pipe stdin của ssh đóng theo nó, và giao thức hứa daemon giết mọi thứ khi stdin kết thúc; nếu EOF không tới được máy từ xa thì giới hạn 30 giây im lặng vẫn dọn dẹp (protocol.md:64–65; đường đi của EOF qua ssh là suy luận, không có test trong repo). Khi mở lại, Durable xử lý lời gọi tool bị cắt ngang theo chính sách replay của tool (chương task).

pi-telemetry: hợp đồng span, sự kiện, metric#

Bài toán: pi-ai, pi-agent-core và Pi Durable đều muốn mô tả việc chúng làm (một request tới mô hình mất bao lâu, một phase của task kết thúc ra sao), nhưng người dùng của chúng dùng backend quan sát khác nhau: OpenTelemetry, Sentry, log thuần, hay không gì cả. Gắn một SDK cụ thể vào thư viện là bắt mọi người theo nó. pi-telemetry là "vendor-neutral telemetry contracts and typed schema utilities": một hợp đồng span dựa trên callback, một context no-op dùng chung, propagation qua tiến trình, metric, một adapter tham chiếu trong bộ nhớ, schema có kiểu, và không có exporter, không có span "hiện tại" toàn cục, không phụ thuộc backend nào (README.md:3–15). Package được tách ra ở commit 6b461b75b "feat: extract telemetry package" (2026-08-05); bản 1.4.0 thêm propagation, link, metric, TelemetryStore và renderTrace (CHANGELOG.md:3–20).

Từ vựng theo bảng của README (README.md:48–55): span là bản ghi có thời gian của một thao tác; span lồng nhau thành cây; attribute là một sự kiện có tên gắn vào span (cache.hit: true); event là một điểm trong span, không có độ dài; status là ok hoặc error; context là nơi một span mới sẽ được gắn vào cây. Span là dữ liệu chẩn đoán, không phải state nghiệp vụ.

Không có end(). startSpan(options, callback) giữ span mở cho tới khi giá trị hay promise của callback xong; thành công là ok, ném hay reject là error trừ khi đã đặt status tường minh (README.md:108–121). Hợp đồng của một adapter được liệt kê thành từng điều: gọi callback đồng bộ đúng một lần, giữ nguyên giá trị trả về và giá trị reject, gộp setAttributes (giá trị sau thay giá trị trước, undefined bị bỏ qua), các phương thức ghi là đồng bộ, và bỏ qua mọi lời gọi sau khi span đã xong (README.md:127–139). Đầu vào có kiểu được tin; adapter không cần kiểm hay nuốt lỗi của chính nó (CHANGELOG 1.4.0, CHANGELOG.md:7).

No-op và adapter trong bộ nhớ#

NOOP_TELEMETRY_CONTEXT: một span trơ đông cứng dùng chung, kể cả cho span con
pi/packages/telemetry/src/noop.tsdòng 4–37TypeScript
async function startNoopSpan<T>(_options: SpanOptions, callback: (span: TelemetrySpan) => T | Promise<T>): Promise<T> {	return callback(noopTelemetrySpan);}const noopCounter: TelemetryCounter = Object.freeze({ add: () => {} });const noopUpDownCounter: TelemetryUpDownCounter = Object.freeze({ add: () => {} });const noopHistogram: TelemetryHistogram = Object.freeze({ record: () => {} });/** Shared instruments that never inspect or retain metadata, measurements, or contexts. */export const NOOP_TELEMETRY_METRICS: TelemetryMetrics = Object.freeze({	counter: () => noopCounter,	upDownCounter: () => noopUpDownCounter,	histogram: () => noopHistogram,});/** Shared propagation capability that neither inspects nor retains its inputs. */export const NOOP_TELEMETRY_PROPAGATION: TelemetryPropagation = Object.freeze({	inject: () => undefined,	extract: () => NOOP_TELEMETRY_CONTEXT,});const noopTelemetrySpan: TelemetrySpan = {	metrics: NOOP_TELEMETRY_METRICS,	propagation: NOOP_TELEMETRY_PROPAGATION,	startSpan: startNoopSpan,	addLink: () => {},	addEvent: () => {},	setAttributes: () => {},	setStatus: () => {},};Object.freeze(noopTelemetrySpan);/** Shared telemetry context used when an application does not provide one. */export const NOOP_TELEMETRY_CONTEXT: TelemetrySpan = noopTelemetrySpan;
async là có chủ đích: một lần ném đồng bộ trong callback thành một promise bị reject với đúng giá trị đó. Đây là span mà BACKGROUND_CONTEXT của Chord trả về khi không có span nào (chương Chord).

InMemoryTelemetryContext là hiện thực tham chiếu, dùng cho test, chẩn đoán cục bộ, hoặc ứng dụng muốn ghi lại trong tiến trình mà không cần exporter. Nó cài đặt cùng lúc TelemetryContext, TelemetryPropagation, TelemetryMetrics và TelemetryStore, ghi span với id số tăng dần, và trả về ảnh chụp đã tách khỏi bộ nhớ của nó (memory.ts:282–299). Phần quyết định status và kết thúc:

startInMemorySpan(): span cha đã xong thì span con trơ; lỗi thành status error nếu chưa đặt tường minh
pi/packages/telemetry/src/memory.tsdòng 182–191, 230–246TypeScript
async function startInMemorySpan<T>(	state: InMemoryTelemetryState,	parent: InMemoryParent | undefined,	options: SpanOptions,	callback: (span: TelemetrySpan) => T | Promise<T>,): Promise<T> {	if (parent?.span?.settled) return startInertSpan(state, options, callback);	const recordedSpan = createSpan(state, parent, options);	state.spans.push(recordedSpan);	contextCarriers.set(span, recordedSpan.context);	try {		return await callback(span);	} catch (error) {		if (!recordedSpan.explicitStatus)			recordedSpan.status =				error instanceof Error					? { status: "error", error: { name: error.name, message: error.message } }					: { status: "error" };		throw error;	} finally {		recordedSpan.settled = true;		recordedSpan.endedMs = recordedSpan.startedMs + performance.now() - recordedSpan.clock;		recordedSpan.endSequence = state.nextEndSequence++;	}}
Bộ nhớ không có trần và chỉ trong tiến trình; README khuyên tạo instance mới cho mỗi phạm vi ghi và không ghi thuộc tính nhạy cảm (README.md:285).

Để một trace đi qua ranh giới tiến trình, adapter có thể cài đặt TelemetryPropagation: inject(context) trả về một carrier, một map chuỗi sang chuỗi an toàn với JSON mà định dạng do adapter quyết (W3C traceparent cũng vừa), và extract(carrier) trả về một context cha ở phía nhận; carrier lạ hay vắng thì rơi về gốc. Transport chỉ chở carrier, không bao giờ chở context hay span; và metadata của trace là chẩn đoán, không bao giờ là phân quyền (README.md:143–167). Adapter trong bộ nhớ dùng ba khoá pi-memory-scope, pi-memory-span, pi-memory-trace, nên carrier của nó sống qua JSON và qua các tiến trình độc lập (memory.ts:307–324). Link nối hai đoạn công việc mà không đổi cha, ví dụ một activation với mọi lời gọi đã kích nó (README.md:169–181).

Metric (counter, up-down counter, histogram) là một năng lực tuỳ chọn của cùng adapter, lộ ra qua context.metrics, để một lời gọi mô hình lồng sâu vẫn ghi được metric mà không cần cấu hình riêng (metrics.ts:10–41). Histogram nhận thêm một context để gắn phép đo với span (exemplar). Quy tắc: chỉ dùng thuộc tính có ít giá trị (hướng, phương thức), không dùng id request, id người dùng, prompt hay đường dẫn tuỳ ý (README.md:205–209).

Đọc lại: TelemetryStore và renderTrace#

TelemetryStore tách truy vấn khỏi ghi nhận: findTraces(query) (trace mới nhất trước, lọc theo tên span sớm nhất, khoảng thời gian, errors, thuộc tính, phân trang bằng cursor), getTrace(traceId), findMetrics(query) (index.ts:90–95). Hai hiện thực: adapter trong bộ nhớ và telemetry-sqlite của Chord (chương Chord). Store chỉ trả span đã xong; trace id là span id của gốc, nên span từ nhiều store được gộp theo trace id, và quan hệ cha con, không phải việc thời gian nằm lồng nhau, định nghĩa cây (README.md:289–297). renderTrace(spans) in một rừng span theo thời gian, coi cha vắng mặt là gốc, kèm thuộc tính, thời lượng, lỗi, offset của event và link (render.ts:9–49).

Bộ conformance#

@earendil-works/pi-telemetry/testing xuất createTelemetryAdapterConformance(factory): 14 ca kiểm độc lập với test runner, mỗi ca một fixture mới; fixture cung cấp context và đổi span đã xong của backend thành RecordedTelemetrySpan chuẩn hoá (testing/types.ts:4–21). Bộ ca kiểm năng lực được mang qua mọi context, việc nhận callback đồng bộ đúng một lần, danh tính kết quả và lỗi, status tự động và tường minh, gộp thuộc tính, thứ tự event, lời gọi trơ sau khi xong, quan hệ cha con lồng và đồng thời, link; với fixture có propagation và metrics thì kiểm thêm vòng JSON, nhiều chặng adapter, và thứ tự phép đo (README.md:341). Đây là cách một adapter OpenTelemetry hay Sentry do người khác viết chứng minh nó tuân hợp đồng.

Schema telemetry có kiểu#

API tầng thấp cố ý mở: tên span là chuỗi bất kỳ, thuộc tính là một túi bất kỳ, để adapter dùng chung được. Package miền (pi-ai, Durable) muốn ngược lại: một từ vựng đóng, để mọi chỗ phát ra đúng tên và đúng thuộc tính. Schema là dữ liệu JSON tuần tự hoá được, mô tả từng span (mô tả, quy tắc cha, thuộc tính lúc bắt đầu kèm required, thuộc tính lúc hoàn tất, event, quy tắc status) và từng metric (index.ts:128–177). defineTelemetrySchema() chỉ là hàm định danh có kiểu: nó trả lại chính đối số và không kiểm gì lúc chạy.

createTypedSpanStarter(): lúc chạy chỉ là startSpan; mọi việc kiểm nằm ở kiểu
pi/packages/telemetry/src/index.tsdòng 427–457TypeScript
function bindTypedSpanStarter<Schemas extends TelemetrySchemaTuple>(	telemetryContext: TelemetryContext,): TypedSpanStarter<Schemas> {	const startSpan = (		name: SpanNameInSchemas<Schemas>,		attributes: SpanAttributes,		callback: (			span: SpanInSchema<Schemas[number], SpanNameInSchemas<Schemas>>,			startChildSpan: TypedSpanStarter<Schemas>,		) => unknown,	): Promise<unknown> =>		telemetryContext.startSpan({ name, attributes }, (span) =>			callback(				span as SpanInSchema<Schemas[number], SpanNameInSchemas<Schemas>>,				bindTypedSpanStarter<Schemas>(span),			),		);	return startSpan as TypedSpanStarter<Schemas>;}/** * Bind an explicit parent context to the combined span vocabulary of one or more schemas. * Schema values are used only for type inference; no runtime schema validation is performed. */export function createTypedSpanStarter<const Schemas extends TelemetrySchemaTuple>(	telemetryContext: TelemetryContext,	_schemas: Schemas & UniqueTelemetrySchemas<Schemas>,): TypedSpanStarter<Schemas> {	return bindTypedSpanStarter<Schemas>(telemetryContext);}
Callback nhận thêm một starter con đã gắn vào span vừa mở. Kiểu (định nghĩa ở phần trên của file) có một overload cho mỗi span: tên lạ, thiếu thuộc tính bắt buộc, khoá thừa, giá trị ngoài tập đóng, event không khai báo đều là lỗi biên dịch; tên span trùng giữa các schema trong một mảng cũng vậy (UniqueTelemetrySchemas).

startAttributes và endAttributes chỉ nói khi nào thuộc tính thường đã biết; cả hai thành thuộc tính thường của cùng một span, và "end" nghĩa là làm giàu lúc hoàn tất, luôn tuỳ chọn, vì một đường lỗi sớm hay huỷ có thể không bao giờ có giá trị đó (README.md:424–453). createTypedMetrics(metrics, schemas) làm việc tương tự cho metric, khác ở chỗ nó đọc và giữ metadata của instrument (mô tả, đơn vị) lúc chạy để chuyển cho adapter; nó vẫn không kiểm thuộc tính lúc chạy (metrics.ts:128–145).

Pi Durable và telemetry#

Host đưa một adapter vào HarnessOptions.telemetry; vắng thì là no-op (durable/harness/types.ts:609–611). Durable dùng nó cho hai span ngắn và vài metric, mô tả bằng DURABLE_TELEMETRY_SCHEMA. Lời hứa quan trọng nhất nằm ở README: thuộc tính chỉ chứa ID và metadata điều khiển, không bao giờ chứa message, đối số hay kết quả; ID trỏ vào transcript (durable/README.md:82).

Span phase được mở trong scheduler. Đoạn đầu cho thấy hai ý thiết kế cùng lúc:

#phase(): cha là carrier lưu trong task; span được đưa xuống Context của invocation
pi/packages/durable/src/harness/scheduler.tsdòng 1739–1765TypeScript
/** A phase owns its decision commit, not the next handler, a durable wait, or recovery. */#phase<T>(invocation: Invocation, task: ErasedRunningTask, run: () => Promise<T>): Promise<T> {	const parent =		task.trace === undefined			? this.#telemetry			: (this.#telemetry.propagation?.extract(task.trace) ?? this.#telemetry);	const checkpoint = task.state.checkpoint;	const attempt = checkpoint.attempt;	const resumed = this.#resumed.delete(task.id);	return createTypedSpanStarter(parent, [DURABLE_TELEMETRY_SCHEMA])(		"pi.durable.phase",		{			"task.id": task.id,			"task.name": task.kind,			phase: checkpoint.phase,			kind: invocation.mode,			resumed,			...(attempt === undefined ? {} : { attempt }),		},		async (span) => {			if (span.propagation !== undefined && invocation.links !== undefined) {				for (const carrier of invocation.links) span.addLink({ context: span.propagation.extract(carrier) });			}			const previous = invocation.context;			invocation.context = withTelemetry(span, previous);			const started = performance.now();			try {
Phần sau đặt outcome, đặt status lỗi với tên task_faulted, task_failed, task_orphaned và thông báo cắt còn 500 ký tự, rồi ghi thời lượng vào histogram kèm span làm exemplar (scheduler.ts:1766–1802).

Ý thứ nhất: cha của span không phải context đang chạy mà là task.trace, một carrier được inject lúc task được tạo và lưu trong bản ghi task (durable/types.ts:646–650; tạo ở transaction.ts:435–438). Một task chạy qua nhiều lần khởi động lại, ở nhiều tiến trình khác nhau; nhờ carrier nằm trong storage, phase chạy sau một lần crash vẫn gắn vào trace của nơi đã tạo ra task. README của pi-telemetry cho phép đúng điều này: context và span không bao giờ được lưu, nhưng một carrier đã inject có thể được lưu làm dữ liệu tương quan chẩn đoán tuỳ chọn (README.md:515). Submission cũng lưu carrier của span admission (submissions.ts:219). Ý thứ hai: withTelemetry(span, previous) của Chord đặt span vào Context của invocation, nên mọi thứ phase gọi xuống (pi-ai, tool) đều nhận span làm cha qua tham số, không qua ngữ cảnh ngầm.

Nguyên tắc "không chứa nội dung" được giữ cả ở đường lỗi: khi admission thất bại, span chỉ nhận status error mà không có thông báo, vì "Error text may contain caller content" (submissions.ts:93–97). Còn lưu trace ở đâu là việc của host: một adapter OpenTelemetry, hoặc createSqliteTelemetry(db) của Chord trên chính database SQLite của ứng dụng. Theo README, pi-ai nhận một TelemetrySpan cha qua tuỳ chọn telemetry của request và xuất AI_TELEMETRY_SCHEMA; pi-agent-core giữ instrumentation và schema riêng của nó (README.md:497–511).

pi-evals: đo hành vi của coding agent#

packages/evals là "behavioral evals for Pi's coding agent, built with vitest-evals" (README.md:3). Nó là package riêng tư, không phát hành (package.json:4), xuất hiện từ commit eafe11fb9 "feat(coding-agent): add vitest eval harness (#7085)" (2026-07-25). Đối tượng được đo là coding agent (AgentSession, xem chương AgentSession), không phải Pi Durable, và mỗi lần chạy gọi một mô hình thật: cần PI_PROVIDER và PI_MODEL cùng credential trong ~/.pi/agent/auth.json (README.md:22–28; docker.ts:67–89).

Hai loại eval#

  • Host eval (*.eval.ts), chạy bằng Vitest ngay trên máy (npm run eval:host): smoke.eval.ts hỏi thủ đô của Pháp với noTools: "all" và kiểm câu trả lời, lỗi, provider, số token (smoke.eval.ts:5–18); documentation-audit.eval.ts cho agent đối chiếu từng trang trong packages/coding-agent/docs với code, và nộp kết luận match/mismatch/inconclusive qua một tool tuỳ chỉnh có schema chặt (documentation-audit.eval.ts:9–65).
  • Eval độ nâng của tài liệu (documentation lift, *.docs.eval.ts): mỗi ca chạy trong hai container cô lập, without_docs và with_docs, và báo cáo chênh lệch tỉ lệ đạt (README.md:5–10). Năm bộ ở v1.4.0: custom provider, extension, model, provider OpenAI, TUI.

Một eval độ nâng trông như thế này: cấu hình một mô hình mới cho Pi đang chạy, nạp lại, rồi so cấu hình thật với kết quả mong đợi bằng một judge khớp chặt:

models.docs.eval.ts: chấm bằng trạng thái thật của runtime, không bằng lời của mô hình
pi/packages/evals/evals/models.docs.eval.tsdòng 25–40TypeScript
const harness = createPiDocumentationEvalHarness({	output: ({ session }) => inspectAddedModel(session.modelRuntime, PROVIDER_ID, MODEL_ID),});const judge = StructuredOutputJudge({ expected, match: "strict", allowExtras: false });describeEval("Add model to existing provider", { harness, judges: [judge], judgeThreshold: null }, (it) => {	it("adds the model without replacing existing models", async ({ run }) => {		await run([			{				type: "prompt",				content: `Configure this running Pi installation with a new \`${PROVIDER_ID}/${MODEL_ID}\` model. Do not create project-local configuration. Show it as “${MODEL_NAME}”. It accepts text, supports reasoning, has a 32,768-token context window and a 4,096-token maximum output, and has no usage cost.`,			},			{ type: "reload" },		]);	});});
judgeThreshold: null nghĩa là điểm thấp là dữ liệu, không phải lỗi hạ tầng; assertion của Vitest chỉ dành cho bất biến của bộ test bị vỡ (README.md:127–129).

Harness: một coding agent cô lập cho mỗi lần chạy#

createPiCodingAgentHarness() là bộ chuyển đổi cho vitest-evals. Mỗi lần chạy, runPiCodingAgent() tạo một thư mục gốc tạm với workspace, home và thư mục agent riêng, đổi HOME và PI_CODING_AGENT_DIR sang đó, nạp đúng credential của provider đã chọn vào một InMemoryCredentialStore, gieo file fixture vào workspace (từ chối đường dẫn thoát ra ngoài), dựng session với thinkingLevel: "off", và từ chối nếu có extension nào ngoài những extension inline mà chính harness thêm (harness.ts:272–365). Sau khi chạy, nó gom transcript thành sự kiện, usage (token, lời gọi tool, chi phí ước tính nếu mô hình có giá), hash SHA-256 của system prompt, và lưu file session.jsonl làm artifact (harness.ts:390–431). Biến thể tài liệu được dựng ở đây:

Biến thể without_docs: cắt mục docs khỏi system prompt mặc định; chỉ tool file, không shell
pi/packages/evals/src/harness.tsdòng 484–506, 523–537TypeScript
/** Documentation evals intentionally exclude shell and unrestricted network tools. */export const DOCUMENTATION_EVAL_TOOLS = ["read", "write", "edit", "grep", "find", "ls"] as const;export function resolveDocumentationVariant(	value: string | undefined = process.env.PI_EVAL_VARIANT,): DocumentationVariant {	if (value === "without_docs" || value === "with_docs") return value;	throw new TypeError('PI_EVAL_VARIANT must be "without_docs" or "with_docs".');}export function excludePiDocumentation(defaultPrompt: string): string {	const documentationStartMarker = "\n<docs>\n";	const documentationEndMarker = "\n</docs>";	const documentationStart = defaultPrompt.indexOf(documentationStartMarker);	if (documentationStart === -1) throw new Error("Default Pi system prompt has no Pi documentation section.");	const documentationEnd = defaultPrompt.indexOf(documentationEndMarker, documentationStart);	if (documentationEnd === -1) throw new Error("Default Pi system prompt has no complete Pi documentation section.");	const cwdStart = defaultPrompt.lastIndexOf("\n<cwd>\n");	if (cwdStart < documentationEnd) throw new Error("Default Pi system prompt has no working-directory section.");	return (		defaultPrompt.slice(0, documentationStart) + defaultPrompt.slice(documentationEnd + documentationEndMarker.length)	);}export function createPiDocumentationEvalHarness<TOutput extends JsonValue>(	options: DocumentationHarnessOptions | DocumentationHarnessWithOutput<TOutput> = {},) {	if (process.env.PI_EVAL_CONTAINER !== "1" || !resolveSandboxIdentity()) {		throw new Error("Documentation evals must run in the isolated container sandbox.");	}	const variant = resolveDocumentationVariant();	return createPiCodingAgentHarness({		...options,		name: variant,		tools: options.tools ?? [...DOCUMENTATION_EVAL_TOOLS],		...(variant === "without_docs" ? { transformSystemPrompt: excludePiDocumentation } : {}),		expectedPiDocumentation: variant === "with_docs",	});}
Ngoài prompt, image without_docs còn xoá hẳn README.md, CHANGELOG.md, docs/ và examples/ của coding agent (README.md:82–90). verifySystemPrompt kiểm prompt thật của mỗi lần chạy có đúng biến thể không (harness.ts:257–270).

Container: mô hình không đọc được bài chấm#

Một eval có thể bị "giải" bằng cách đọc judge hay fixture. Thiết kế chặn điều đó bằng quyền file. Dockerfile build workspace một lần, dựng hai target without_docs và with_docs từ cùng một runtime đã cài, và đặt mọi mã nguồn của evaluator ở chế độ chỉ root đọc được (docker/Dockerfile:12–44). Entrypoint kiểm nội dung image theo danh sách cho phép, kiểm tài liệu của package nội bộ không lộ cho mô hình, chạy thử một tiến trình với UID của sandbox để chắc nó không đọc được mã evaluator, và kiểm coding agent được nạp từ dist/ (docker/entrypoint.ts:27–105). Bên trong lần chạy, harness tự hạ quyền vĩnh viễn trước khi gửi prompt:

enterToolSandbox(): bắt đầu là root để nạp evaluator, rồi setuid xuống UID không đặc quyền và tự kiểm
pi/packages/evals/src/harness.tsdòng 154–188TypeScript
async function enterToolSandbox(root: string, identity: SandboxIdentity | undefined): Promise<void> {	if (!identity) return;	if (		typeof process.getuid !== "function" ||		typeof process.geteuid !== "function" ||		typeof process.setuid !== "function" ||		typeof process.setgid !== "function" ||		typeof process.setgroups !== "function"	) {		throw new Error("The eval filesystem sandbox requires POSIX user APIs.");	}	if (process.getuid() !== 0 || process.geteuid() !== 0) {		throw new Error("The eval runner must start as root before entering the unprivileged tool sandbox.");	}	const protectedPaths = await protectTransformedModules();	await chownTree(root, identity);	process.setgroups([]);	process.setgid(identity.gid);	process.setuid(identity.uid);	if (process.getuid() !== identity.uid || process.geteuid() !== identity.uid) {		throw new Error("Failed to enter the unprivileged eval tool sandbox.");	}	for (const path of protectedPaths) {		try {			await readFile(path);		} catch (error) {			if (error instanceof Error && "code" in error && (error.code === "EACCES" || error.code === "ENOENT")) {				continue;			}			throw error;		}		throw new Error(`The unprivileged eval process can read a transformed eval module: ${path}`);	}}
Module đã được Vite biến đổi (bản biên dịch của file eval) được chmod 600 trước khi hạ quyền, rồi harness thử đọc lại từng file: đọc được là lỗi. Mỗi container chạy --read-only, /tmp là tmpfs, credential được mount chỉ đọc, UID sandbox là 65532 (docker.ts:91–124).

README nói rõ giới hạn: tool của eval tài liệu chỉ gồm read, write, edit, grep, find, ls, nhưng container vẫn cần mạng để gọi provider, nên Docker một mình không chứng minh được code do agent viết không dùng mạng (README.md:90).

Kế hoạch, chạy, ghép cặp#

src/cli.ts điều phối một lần so sánh: build hai image (id phải khác nhau), phát hiện ca trong cả hai biến thể và đòi hai tập ca giống hệt, ghi protocol.json kèm digest, chạy từng nhiệm vụ trong một container mới (tuần tự), ghi observations.jsonl sau mỗi nhiệm vụ, rồi tóm tắt; còn cặp bị chặn thì thoát mã khác 0 (cli.ts:114–192). Kế hoạch được lập trước khi chạy bất cứ thứ gì:

createTaskPlan(): mỗi (ca, lần chạy) có hai nhánh, thứ tự đảo theo số lần chạy
pi/packages/evals/src/plan.tsdòng 39–59TypeScript
export function createTaskPlan(	cases: readonly DiscoveredEvalCase[],	model: string,	runsPerVariant: number,): EvalTask[] {	if (!model.includes("/") || model.startsWith("/") || model.endsWith("/")) {		throw new TypeError("Model identity must contain a provider and model.");	}	if (!Number.isSafeInteger(runsPerVariant) || runsPerVariant < 1) {		throw new TypeError("Runs per variant must be a positive integer.");	}	const tasks: EvalTask[] = [];	for (const evalCase of cases) {		for (let runNumber = 1; runNumber <= runsPerVariant; runNumber += 1) {			const variants: DocumentationVariant[] =				runNumber % 2 === 1 ? ["without_docs", "with_docs"] : ["with_docs", "without_docs"];			for (const variant of variants) tasks.push({ ...evalCase, variant, model, runNumber });		}	}	return tasks;}
Đảo thứ tự without_docs/with_docs giữa các lần lặp là để giảm thiên lệch do thứ tự (README.md:80). Mặc định một lần mỗi biến thể; tăng bằng --runs-per-variant khi cần đo độ ổn định (README.md:57–63).

Báo cáo ghép từng cặp (ca, mô hình, lần chạy). Một cặp chỉ được tính khi mỗi nhánh có đúng một quan sát có điểm; thiếu, trùng, bị bỏ qua, chưa chạy, không chấm hay lỗi đều làm cặp bị chặn, kèm lý do (report.ts:249–281). "Đạt" nghĩa là điểm ≥ 1. Tỉ lệ đạt của từng bộ và độ nâng (treatment − control) chỉ được công bố khi bộ đó không có cặp nào bị chặn; token, lời gọi tool, thời gian và chi phí được so trung bình theo cặp, và telemetry vắng mặt được giữ là "không có" chứ không coi là 0 (report.ts:359–413; README.md:103–105). Báo cáo còn gắn cờ no-lift, negative-delta, control-saturated, treatment-saturated và flaky (cùng ca, cùng biến thể mà lúc đạt lúc không) (report.ts:336–357). "Telemetry" trong ngữ cảnh này là số liệu usage do harness gom, không liên quan tới pi-telemetry.

Quyết định thiết kế#

Một chuẩn đối chiếu duy nhất, và test để giữ nó#

Mục tiêu của pi-env không phải "một môi trường từ xa hợp lý" mà là "giống hệt NodeExecutionEnv chạy ở đó". Đó là lý do có bảng chia việc: luật nằm ở client bằng TypeScript, system call ở daemon. Mục tiêu đó được kiểm bằng hai cách: bộ kiểm tuân thủ env của Durable chạy trên RemoteExecutionEnv qua một pipe và trên LocalExecutionEnv (test/conformance.test.ts:17–45), và một test vi sai dựng một cặp môi trường cục bộ và từ xa trên cùng máy rồi so các chuỗi thao tác ngẫu nhiên và chính các tool của Durable (test/differential.test.ts:24–33; README.md:63–66). Không có test như vậy, mọi khác biệt nhỏ (thứ tự readdir, một mã lỗi) sẽ lộ ra như một tool hành xử khác khi đổi máy.

SSH làm xác thực, daemon không mở cổng#

Daemon không lắng nghe trên mạng; nó chỉ nói chuyện qua stdin/stdout của một phiên SSH. Xác thực, mã hoá và quyền là việc của SSH, và pi-env siết SSH tới mức tối thiểu: không chuyển tiếp gì, không dùng chung kết nối, không lệnh từ cấu hình, host key nghiêm theo file và alias riêng, key đổi không bao giờ được tự chấp nhận. Binary được đặt tên theo nội dung và được kiểm hash trước mỗi lần khởi động, nên một binary bị sửa trên máy từ xa không được chạy, và hai phiên bản không bao giờ đè lên nhau (README.md:20–21).

Không mồ côi, đổi lại không tiếp tục#

Daemon giết mọi thứ khi kết nối im 30 giây hay stdin kết thúc. Lựa chọn ngược lại (để lệnh chạy tiếp và cho client nối lại) sẽ cần id phiên, đệm output và một giao thức tiếp tục; đổi lại, nó để lại tiến trình không ai quản khi một điện thoại mất mạng. pi-env chọn đơn giản và an toàn: lệnh chết theo kết nối, request đang bay thất bại với lost: true, và với thao tác có hiệu ứng, kết quả là "không biết". Đây cũng là ngữ nghĩa mà Durable đã có cho tool bị cắt ngang, nên hai tầng khớp nhau.

Câu trả lời nhỏ không chờ dữ liệu lớn#

Một hàng control và một hàng bulk ở daemon, cộng cửa sổ output, giải hai vấn đề của link chậm: ping và kết quả của một lệnh huỷ không bị kẹt sau megabyte output, và output mà người gọi sẽ vứt đi thì không bao giờ được gửi. Cả hai dựa vào thông tin chỉ người gọi biết (nó giữ phần đuôi bao nhiêu, commit nhanh cỡ nào), nên Durable mới thêm ShellOutputWindow vào giao diện ExecutionEnv.

Telemetry: callback, tham số tường minh, không chứa nội dung#

Span dựa trên callback (không có end()) nên không thể quên đóng span hay đóng hai lần, và status tự suy ra từ kết quả của callback. Context đi theo tham số chứ không theo AsyncLocalStorage, nên package chạy được trong Node, Bun, trình duyệt và worker (README.md:519), và hai lời gọi đồng thời không bao giờ lẫn cha. Đầu vào được tin để đường ghi không tốn công kiểm; kiểm nằm ở kiểu, qua schema. Và các schema của Pi cố ý chỉ chứa ID và metadata điều khiển, để bật telemetry không bao giờ có nghĩa là gửi prompt hay output của tool cho một bên thứ ba.

Eval: thà không có con số còn hơn một con số sai#

Ghép cặp chính xác theo (ca, mô hình, lần chạy), lập kế hoạch toàn bộ trước khi chạy, và giữ lại tỉ lệ đạt khi có bất kỳ cặp nào bị chặn: thiết kế này chấp nhận mất kết quả của cả một lần chạy dài thay vì công bố một độ nâng tính trên một tập con không đầy đủ. Cô lập bằng quyền file (mã evaluator chỉ root đọc được, harness hạ quyền trước khi gọi mô hình) để mô hình không thể đọc bài chấm.

Cạm bẫy và trường hợp biên#

  • Mất kết nối giữa một thao tác ghi. Request đang bay thất bại với { code: "unknown", lost: true }; nó có thể đã chạy xong ở xa. isConnectionLost(error) phân biệt trường hợp này với một lỗi daemon trả về (connection.ts:125–128).
  • Handle không sống qua lần khởi động lại. Reader đang mở khi kết nối mất sẽ thất bại ở lần đọc kế, kể cả khi kết nối đã có lại; mở lại reader.
  • Laptop ngủ là lệnh từ xa chết. 30 giây không có byte nào là daemon giết process group. Build dài trên máy từ xa không sống qua lúc client ngủ.
  • Lần khởi động nào cũng tốn thêm vòng SSH. Phát hiện hệ thống chỉ một lần cho mỗi sshConnection, nhưng kiểm hash daemon chạy lại ở mỗi lần khởi động, trước request đầu tiên sau một lần mất kết nối.
  • Môi trường đăng nhập. Mặc định daemon không chạy qua shell đăng nhập, nên lệnh không thấy những gì ~/.profile đặt; bật loginShell: true nếu cần (ssh.ts:463–472). Locale cũng không được chuyển tiếp.
  • Daemon cũ. listFiles trả not_supported với daemon trước 1.2.0; người gọi phải có đường duyệt cây của mình.
  • Windows và Termux. Terminal chưa có trên Windows; chuỗi lệnh trên Windows chạy qua Git Bash như NodeExecutionEnv, argv chạy thẳng (README.md:58–59). Termux cần TMPDIR, termux-exec và termux-wake-lock.
  • Known-hosts riêng. ~/.ssh/known_hosts và file toàn cục không được đọc; key phải được chấp nhận vào file của ứng dụng, dưới alias của môi trường.
  • Telemetry không phải nơi chứa dữ liệu. Đừng lưu TelemetryContext hay span vào bản ghi; chỉ carrier được phép, và chỉ như dữ liệu tương quan tuỳ chọn. Adapter trong bộ nhớ không có trần; metric chỉ nên có thuộc tính ít giá trị (README.md:513–517).
  • Adapter tự viết phải mang năng lực đi theo. Từ 1.4.0, metrics và propagation phải có trên gốc, trên mọi span con và trên context đã extract, kể cả span trơ; bộ conformance kiểm điều đó (CHANGELOG.md:9).
  • Eval tốn tiền và lộ nội dung. Mỗi nhánh gọi mô hình thật; artifact chứa prompt, câu trả lời, code sinh ra và output của tool (README.md:107). Một lần lặp không chứng minh được độ ổn định. Eval tài liệu từ chối chạy ngoài container sandbox (harness.ts:526–528).

Tài liệu lệch code#

  • README của pi-env nói daemon được "named by its SHA-256"; tên file thật dùng 32 ký tự hex đầu (128 bit) của hash (ssh.ts:380), còn việc kiểm trước khi chạy so cả hash đầy đủ.
  • RFC 0019 ghi endpoint pi.dev/install?version=…; coding agent ở v1.4.0 gọi /api/report-install (xem mục trên).

Đọc code ở đâu#

Thứ tự đọc cho từng package; đường dẫn tính từ gốc repo.

#FileĐọc để hiểu
1env/README.md, docs/protocol.md, docs/semantics.mdHai nửa của pi-env, giao thức frame, luật khớp với NodeExecutionEnv
2durable/src/env/index.tsGiao diện ExecutionEnv, ShellOutputWindow, ShellOutputInfo.skipped
3env/src/connection.tsKhởi động lười, dòng đồng bộ, frame, session, ping, lost
4env/src/ssh.tsTham số SSH, host key, phát hiện hệ thống, triển khai daemon
5env/src/remote-env.ts, errors.tsHiện thực ExecutionEnv: đường dẫn, đọc/ghi theo khúc, exec, cleanup
6watch.ts, list-files.ts, terminal.tsWatcher nối lại, listFiles, LocalExecutionEnv, terminal
7daemon/src/main.rs, frame.rs, output.rsVòng frame, phân luồng, control trước bulk, liveness
8exec.rs, window.rs, sys/unix.rsChạy lệnh, cửa sổ output, process group
9watch.rs, list.rs, pty.rsWatcher cạnh file, liệt kê theo gitignore, pseudo-terminal
10test/conformance.test.ts, test/differential.test.tsCách lời hứa "giống NodeExecutionEnv" được kiểm
11telemetry/README.mdKhái niệm, hợp đồng adapter, propagation, metric, store, schema, bảo mật
12telemetry/src/index.ts, metrics.tsMọi kiểu của hợp đồng, schema và starter có kiểu
13noop.ts, memory.ts, render.tsAdapter no-op, adapter tham chiếu, in trace
14testing/conformance.tsHợp đồng adapter ở dạng chạy được
15durable/src/harness/telemetry.ts, submissions.tsSchema của Durable; span submit; #phase trong scheduler.ts
16evals/README.mdHai loại eval, cách chạy, biến thể tài liệu, đọc kết quả
17evals/src/harness.tsMột coding agent cô lập cho mỗi lần chạy, sandbox, biến thể prompt
18cli.ts, plan.ts, docker.ts, report.tsĐiều phối, kế hoạch, container, ghép cặp và độ nâng
19docker/Dockerfile, entrypoint.ts, evals/Hai image, kiểm image, các bộ eval

Tóm tắt#

  • pi-env cho Pi Durable một ExecutionEnv trên máy khác: một daemon Rust nói giao thức frame qua stdin/stdout của SSH, còn worker, storage và credential ở lại máy cục bộ.
  • Client áp mọi luật của Node (đường dẫn, abort, mã lỗi, cách chia lần đọc), daemon làm system call; kết quả khớp NodeExecutionEnv trên máy đó, được giữ bằng conformance và test vi sai.
  • SSH bị siết: không chuyển tiếp, không chia sẻ kết nối, host key nghiêm theo file và alias riêng; daemon đặt tên theo hash và được kiểm trước mỗi lần khởi động.
  • Frame có JSON và payload thô; trả lời đi trước output số lượng lớn; cửa sổ output bỏ phần người gọi sẽ vứt và báo lại bằng số đếm; 30 giây im lặng là daemon giết mọi thứ.
  • pi-telemetry là hợp đồng span dựa trên callback, context tường minh, no-op và adapter trong bộ nhớ, propagation bằng carrier, link, metric, store và bộ conformance; schema có kiểu chỉ kiểm lúc biên dịch.
  • Durable phát span pi.durable.submit và pi.durable.phase cùng ba metric, không chứa nội dung; carrier lưu trong bản ghi task giữ trace qua các lần khởi động lại.
  • pi-evals đo coding agent: host eval chạy tại chỗ, eval tài liệu so with_docs với without_docs trong container cô lập, ghép cặp chính xác và giữ lại con số khi có cặp bị chặn.

Mục lục

Giao diện