@botlearn-course/daemon 0.0.19 → 0.0.20-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/agent-service-sandbox.d.ts +9 -1
- package/dist/agent-service-sandbox.js +523 -39
- package/dist/agent-service-ws-protocol.d.ts +3 -3
- package/dist/agent-service-ws-protocol.js +7 -2
- package/dist/cli.js +19 -1
- package/dist/file-candidates.d.ts +28 -1
- package/dist/file-candidates.js +102 -49
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/run-dispatcher.d.ts +3 -2
- package/dist/run-dispatcher.js +71 -11
- package/dist/runtime-env.js +4 -4
- package/dist/runtime-quiescence.d.ts +19 -0
- package/dist/runtime-quiescence.js +130 -0
- package/dist/runtimes/deepseek-tui.js +11 -0
- package/dist/runtimes/engine.js +1 -1
- package/dist/tool-observation.d.ts +9 -5
- package/dist/tool-observation.js +42 -19
- package/dist/trace-projection.d.ts +21 -0
- package/dist/trace-projection.js +56 -0
- package/dist/types.d.ts +3 -3
- package/dist/workspace-entry-set.d.ts +31 -0
- package/dist/workspace-entry-set.js +164 -0
- package/dist/workspace-filesystem.d.ts +20 -0
- package/dist/workspace-filesystem.js +120 -0
- package/dist/workspace-materialization.d.ts +16 -0
- package/dist/workspace-materialization.js +136 -0
- package/dist/workspace-quota.d.ts +4 -0
- package/dist/workspace-quota.js +42 -0
- package/dist/workspace-restore.d.ts +42 -0
- package/dist/workspace-restore.js +347 -0
- package/dist/workspace-snapshot-control.d.ts +29 -0
- package/dist/workspace-snapshot-control.js +169 -0
- package/dist/workspace-snapshot-policy.d.ts +24 -0
- package/dist/workspace-snapshot-policy.js +45 -0
- package/dist/workspace-snapshot-staging.d.ts +27 -0
- package/dist/workspace-snapshot-staging.js +275 -0
- package/dist/workspace.d.ts +60 -4
- package/dist/workspace.js +576 -9
- package/package.json +1 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
export declare const AGENT_SERVICE_WS_SCHEMA: "botlearn-agent-sandbox-ws/0.
|
|
2
|
-
export declare const AGENT_SERVICE_WS_SUBPROTOCOL: "botlearn-agent-sandbox.
|
|
1
|
+
export declare const AGENT_SERVICE_WS_SCHEMA: "botlearn-agent-sandbox-ws/0.4";
|
|
2
|
+
export declare const AGENT_SERVICE_WS_SUBPROTOCOL: "botlearn-agent-sandbox.v4";
|
|
3
3
|
export declare const WORKSPACE_FILE_READ_BINARY_CAPABILITY: "workspace_file_read_binary_v1";
|
|
4
4
|
export declare const WORKSPACE_FILE_CHUNK_BYTES: number;
|
|
5
5
|
export declare const WORKSPACE_FILE_CHUNK_HEADER_BYTES = 29;
|
|
6
|
-
export type SandboxFrameType = "sandbox.hello" | "sandbox.sync" | "session.open" | "session.activate" | "turn.start" | "turn.cancel" | "session.close" | "sandbox.drain" | "sandbox.shutdown" | "event.ack" | "auth.rotate" | "workspace.file.read" | "ping" | "sandbox.ready" | "sandbox.heartbeat" | "command.ack" | "session.opened" | "session.closed" | "turn.event" | "turn.file.report" | "workspace.file.result" | "sandbox.drained" | "sandbox.log" | "pong" | "protocol.error";
|
|
6
|
+
export type SandboxFrameType = "sandbox.hello" | "sandbox.sync" | "session.open" | "session.activate" | "turn.start" | "turn.cancel" | "session.close" | "sandbox.drain" | "sandbox.shutdown" | "event.ack" | "auth.rotate" | "workspace.file.read" | "workspace.checkpoint" | "workspace.thaw" | "ping" | "sandbox.ready" | "sandbox.heartbeat" | "command.ack" | "session.opened" | "session.open_failed" | "session.closed" | "turn.event" | "turn.file.report" | "workspace.file.result" | "sandbox.drained" | "sandbox.log" | "pong" | "protocol.error";
|
|
7
7
|
export declare class UnsupportedSandboxProtocolError extends Error {
|
|
8
8
|
readonly schemaVersion: unknown;
|
|
9
9
|
constructor(schemaVersion: unknown);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
export const AGENT_SERVICE_WS_SCHEMA = "botlearn-agent-sandbox-ws/0.
|
|
3
|
-
export const AGENT_SERVICE_WS_SUBPROTOCOL = "botlearn-agent-sandbox.
|
|
2
|
+
export const AGENT_SERVICE_WS_SCHEMA = "botlearn-agent-sandbox-ws/0.4";
|
|
3
|
+
export const AGENT_SERVICE_WS_SUBPROTOCOL = "botlearn-agent-sandbox.v4";
|
|
4
4
|
export const WORKSPACE_FILE_READ_BINARY_CAPABILITY = "workspace_file_read_binary_v1";
|
|
5
5
|
export const WORKSPACE_FILE_CHUNK_BYTES = 64 * 1024;
|
|
6
6
|
export const WORKSPACE_FILE_CHUNK_HEADER_BYTES = 29;
|
|
@@ -18,11 +18,14 @@ const FRAME_TYPES = new Set([
|
|
|
18
18
|
"event.ack",
|
|
19
19
|
"auth.rotate",
|
|
20
20
|
"workspace.file.read",
|
|
21
|
+
"workspace.checkpoint",
|
|
22
|
+
"workspace.thaw",
|
|
21
23
|
"ping",
|
|
22
24
|
"sandbox.ready",
|
|
23
25
|
"sandbox.heartbeat",
|
|
24
26
|
"command.ack",
|
|
25
27
|
"session.opened",
|
|
28
|
+
"session.open_failed",
|
|
26
29
|
"session.closed",
|
|
27
30
|
"turn.event",
|
|
28
31
|
"turn.file.report",
|
|
@@ -38,8 +41,10 @@ const SESSION_TYPES = new Set([
|
|
|
38
41
|
"session.activate",
|
|
39
42
|
"session.close",
|
|
40
43
|
"session.opened",
|
|
44
|
+
"session.open_failed",
|
|
41
45
|
"session.closed",
|
|
42
46
|
"workspace.file.read",
|
|
47
|
+
"workspace.checkpoint",
|
|
43
48
|
"workspace.file.result",
|
|
44
49
|
]);
|
|
45
50
|
/** TURN 帧:必须带 runtime_session_id + agent_run_id + worker_attempt + activation_id 四元组。 */
|
package/dist/cli.js
CHANGED
|
@@ -372,7 +372,25 @@ async function cmdAgentServiceSession(args) {
|
|
|
372
372
|
deepseekTuiVersion: process.env.BOTLEARN_MANAGED_DEEPSEEK_TUI_VERSION?.trim() || undefined,
|
|
373
373
|
});
|
|
374
374
|
clearAgentServiceControlEnv();
|
|
375
|
-
|
|
375
|
+
let stopping = false;
|
|
376
|
+
const stop = () => {
|
|
377
|
+
if (stopping)
|
|
378
|
+
return;
|
|
379
|
+
stopping = true;
|
|
380
|
+
void client.stopGracefully().catch((error) => {
|
|
381
|
+
console.error(error instanceof Error ? error.message : "sandbox shutdown failed");
|
|
382
|
+
client.stop();
|
|
383
|
+
});
|
|
384
|
+
};
|
|
385
|
+
process.once("SIGTERM", stop);
|
|
386
|
+
process.once("SIGINT", stop);
|
|
387
|
+
try {
|
|
388
|
+
await client.run();
|
|
389
|
+
}
|
|
390
|
+
finally {
|
|
391
|
+
process.removeListener("SIGTERM", stop);
|
|
392
|
+
process.removeListener("SIGINT", stop);
|
|
393
|
+
}
|
|
376
394
|
return 0;
|
|
377
395
|
}
|
|
378
396
|
// ---------------------------------------------------------------
|
|
@@ -12,11 +12,34 @@ export interface ScannedFile extends RunFileCandidate {
|
|
|
12
12
|
}
|
|
13
13
|
export interface FileReportingClient {
|
|
14
14
|
postFile(agentRunId: string, file: RunFileCandidate): Promise<unknown>;
|
|
15
|
+
recordWorkspaceScanMeasurement?(agentRunId: string, measurement: WorkspaceScanMeasurement): Promise<void> | void;
|
|
16
|
+
}
|
|
17
|
+
export interface WorkspaceScanMeasurement {
|
|
18
|
+
fileCount: number;
|
|
19
|
+
totalBytes: number;
|
|
20
|
+
durationMs: number;
|
|
21
|
+
truncated: boolean;
|
|
15
22
|
}
|
|
16
23
|
export interface FileReportResult {
|
|
17
24
|
reported: number;
|
|
18
25
|
failed: number;
|
|
19
26
|
truncated: boolean;
|
|
27
|
+
scan: WorkspaceScanMeasurement;
|
|
28
|
+
}
|
|
29
|
+
interface WorkspaceFileFingerprint {
|
|
30
|
+
sizeBytes: number;
|
|
31
|
+
sha256: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 普通 Agent turn 启动前捕获的 workspace 文件基线。
|
|
35
|
+
*
|
|
36
|
+
* 基线只用于判断本轮新增/修改,不会上报给 Course Service,也不把迁移复制进来的
|
|
37
|
+
* 既有文件变成本轮 TaskRun 证据。truncated=true 时,缺席于 files 的路径不能安全地
|
|
38
|
+
* 判定为“本轮新增”,所以上报阶段会对这类路径 fail closed。
|
|
39
|
+
*/
|
|
40
|
+
export interface WorkspaceFileBaseline {
|
|
41
|
+
files: ReadonlyMap<string, WorkspaceFileFingerprint>;
|
|
42
|
+
truncated: boolean;
|
|
20
43
|
}
|
|
21
44
|
/**
|
|
22
45
|
* 递归扫描 workspace 产出文件候选。
|
|
@@ -26,8 +49,12 @@ export interface FileReportResult {
|
|
|
26
49
|
export declare function scanWorkspaceFiles(workspaceDir: string, limits?: ScanLimits): Promise<{
|
|
27
50
|
files: ScannedFile[];
|
|
28
51
|
truncated: boolean;
|
|
52
|
+
measurement: WorkspaceScanMeasurement;
|
|
29
53
|
}>;
|
|
54
|
+
/** 捕获普通 Agent turn 开始前的 workspace 基线。 */
|
|
55
|
+
export declare function captureWorkspaceFileBaseline(workspaceDir: string, limits?: ScanLimits): Promise<WorkspaceFileBaseline>;
|
|
30
56
|
/**
|
|
31
57
|
* 扫描并逐个上报文件候选。单文件上报失败只 warn 不中断;返回成功上报数。
|
|
32
58
|
*/
|
|
33
|
-
export declare function reportFileCandidates(client: FileReportingClient, agentRunId: string, workspaceDir: string, log: Logger, limits?: ScanLimits): Promise<FileReportResult>;
|
|
59
|
+
export declare function reportFileCandidates(client: FileReportingClient, agentRunId: string, workspaceDir: string, log: Logger, limits?: ScanLimits, baseline?: WorkspaceFileBaseline): Promise<FileReportResult>;
|
|
60
|
+
export {};
|
package/dist/file-candidates.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
3
|
import { open, readdir } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { performance } from "node:perf_hooks";
|
|
5
6
|
import { redactSecretString } from "./redaction.js";
|
|
6
7
|
const DEFAULT_MAX_FILES = 50;
|
|
7
8
|
// 与后端 settings.daemon_max_file_bytes 默认一致。
|
|
@@ -19,30 +20,50 @@ function isSafeRelativePath(rel) {
|
|
|
19
20
|
const segments = rel.split("/");
|
|
20
21
|
return segments.every((seg) => seg !== "" && seg !== "..");
|
|
21
22
|
}
|
|
22
|
-
async function sha256File(
|
|
23
|
+
async function sha256File(handle) {
|
|
23
24
|
const hash = createHash("sha256");
|
|
24
|
-
const stream = createReadStream(
|
|
25
|
+
const stream = handle.createReadStream({ autoClose: false, start: 0 });
|
|
25
26
|
for await (const chunk of stream)
|
|
26
27
|
hash.update(chunk);
|
|
27
28
|
return hash.digest("hex");
|
|
28
29
|
}
|
|
29
|
-
async function readPreview(
|
|
30
|
-
const
|
|
30
|
+
async function readPreview(handle, maxPreviewChars) {
|
|
31
|
+
const sniff = Buffer.alloc(TEXT_SNIFF_BYTES);
|
|
32
|
+
const { bytesRead } = await handle.read(sniff, 0, TEXT_SNIFF_BYTES, 0);
|
|
33
|
+
if (sniff.subarray(0, bytesRead).includes(0))
|
|
34
|
+
return null;
|
|
35
|
+
// 预览最多 maxPreviewChars 字符;UTF-8 下 4 字节/字符封顶,读够即可。
|
|
36
|
+
const want = maxPreviewChars * 4;
|
|
37
|
+
let buf = sniff.subarray(0, bytesRead);
|
|
38
|
+
if (bytesRead === TEXT_SNIFF_BYTES && want > TEXT_SNIFF_BYTES) {
|
|
39
|
+
const more = Buffer.alloc(want - TEXT_SNIFF_BYTES);
|
|
40
|
+
const extra = await handle.read(more, 0, more.length, TEXT_SNIFF_BYTES);
|
|
41
|
+
buf = Buffer.concat([buf, more.subarray(0, extra.bytesRead)]);
|
|
42
|
+
}
|
|
43
|
+
const text = buf.toString("utf8").replace(/�+$/, "");
|
|
44
|
+
return redactSecretString(text.slice(0, maxPreviewChars));
|
|
45
|
+
}
|
|
46
|
+
function sameFileVersion(left, right) {
|
|
47
|
+
return left.dev === right.dev && left.ino === right.ino && left.size === right.size &&
|
|
48
|
+
left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
|
|
49
|
+
}
|
|
50
|
+
async function scanStableFile(absPath, maxPreviewChars, maxFileBytes) {
|
|
51
|
+
const handle = await open(absPath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
31
52
|
try {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return null;
|
|
36
|
-
// 预览最多 maxPreviewChars 字符;UTF-8 下 4 字节/字符封顶,读够即可。
|
|
37
|
-
const want = maxPreviewChars * 4;
|
|
38
|
-
let buf = sniff.subarray(0, bytesRead);
|
|
39
|
-
if (bytesRead === TEXT_SNIFF_BYTES && want > TEXT_SNIFF_BYTES) {
|
|
40
|
-
const more = Buffer.alloc(want - TEXT_SNIFF_BYTES);
|
|
41
|
-
const extra = await handle.read(more, 0, more.length, TEXT_SNIFF_BYTES);
|
|
42
|
-
buf = Buffer.concat([buf, more.subarray(0, extra.bytesRead)]);
|
|
53
|
+
const before = await handle.stat({ bigint: true });
|
|
54
|
+
if (!before.isFile() || before.size > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
55
|
+
throw new Error("workspace_candidate_invalid_file");
|
|
43
56
|
}
|
|
44
|
-
const
|
|
45
|
-
|
|
57
|
+
const sizeBytes = Number(before.size);
|
|
58
|
+
if (sizeBytes > maxFileBytes)
|
|
59
|
+
return { sizeBytes, tooLarge: true };
|
|
60
|
+
const sha256 = await sha256File(handle);
|
|
61
|
+
const previewText = await readPreview(handle, maxPreviewChars);
|
|
62
|
+
const after = await handle.stat({ bigint: true });
|
|
63
|
+
if (!sameFileVersion(before, after)) {
|
|
64
|
+
throw new Error("workspace_candidate_changed_during_scan");
|
|
65
|
+
}
|
|
66
|
+
return { sizeBytes, tooLarge: false, sha256, previewText };
|
|
46
67
|
}
|
|
47
68
|
finally {
|
|
48
69
|
await handle.close();
|
|
@@ -54,16 +75,16 @@ async function readPreview(absPath, maxPreviewChars) {
|
|
|
54
75
|
* 达到 maxFiles 上限后停止并标记 truncated。
|
|
55
76
|
*/
|
|
56
77
|
export async function scanWorkspaceFiles(workspaceDir, limits = {}) {
|
|
78
|
+
const scanStartedAt = performance.now();
|
|
57
79
|
const maxFiles = limits.maxFiles ?? DEFAULT_MAX_FILES;
|
|
58
80
|
const maxFileBytes = limits.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
|
|
59
81
|
const maxPreviewChars = limits.maxPreviewChars ?? DEFAULT_MAX_PREVIEW_CHARS;
|
|
60
82
|
const maxDepth = limits.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
61
83
|
const files = [];
|
|
62
84
|
let truncated = false;
|
|
63
|
-
let
|
|
85
|
+
let observedFileCount = 0;
|
|
86
|
+
let observedTotalBytes = 0;
|
|
64
87
|
async function walk(dir, depth) {
|
|
65
|
-
if (stop)
|
|
66
|
-
return;
|
|
67
88
|
if (depth > maxDepth) {
|
|
68
89
|
truncated = true;
|
|
69
90
|
return;
|
|
@@ -77,8 +98,6 @@ export async function scanWorkspaceFiles(workspaceDir, limits = {}) {
|
|
|
77
98
|
}
|
|
78
99
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
79
100
|
for (const entry of entries) {
|
|
80
|
-
if (stop)
|
|
81
|
-
return;
|
|
82
101
|
if (entry.name.startsWith(".") || entry.name === "node_modules")
|
|
83
102
|
continue;
|
|
84
103
|
if (entry.isSymbolicLink())
|
|
@@ -93,35 +112,22 @@ export async function scanWorkspaceFiles(workspaceDir, limits = {}) {
|
|
|
93
112
|
const rel = path.relative(workspaceDir, absPath).split(path.sep).join("/");
|
|
94
113
|
if (!isSafeRelativePath(rel))
|
|
95
114
|
continue;
|
|
96
|
-
let
|
|
115
|
+
let scanned;
|
|
97
116
|
try {
|
|
98
|
-
|
|
99
|
-
try {
|
|
100
|
-
sizeBytes = (await handle.stat()).size;
|
|
101
|
-
}
|
|
102
|
-
finally {
|
|
103
|
-
await handle.close();
|
|
104
|
-
}
|
|
117
|
+
scanned = await scanStableFile(absPath, maxPreviewChars, maxFileBytes);
|
|
105
118
|
}
|
|
106
119
|
catch {
|
|
120
|
+
truncated = true;
|
|
107
121
|
continue;
|
|
108
122
|
}
|
|
109
|
-
|
|
123
|
+
const { sizeBytes } = scanned;
|
|
124
|
+
observedFileCount += 1;
|
|
125
|
+
observedTotalBytes += sizeBytes;
|
|
126
|
+
if (scanned.tooLarge) {
|
|
110
127
|
truncated = true;
|
|
111
128
|
continue;
|
|
112
129
|
}
|
|
113
130
|
if (files.length >= maxFiles) {
|
|
114
|
-
truncated = true;
|
|
115
|
-
stop = true;
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
let sha256;
|
|
119
|
-
let previewText;
|
|
120
|
-
try {
|
|
121
|
-
sha256 = await sha256File(absPath);
|
|
122
|
-
previewText = await readPreview(absPath, maxPreviewChars);
|
|
123
|
-
}
|
|
124
|
-
catch {
|
|
125
131
|
truncated = true;
|
|
126
132
|
continue;
|
|
127
133
|
}
|
|
@@ -131,19 +137,66 @@ export async function scanWorkspaceFiles(workspaceDir, limits = {}) {
|
|
|
131
137
|
path: rel,
|
|
132
138
|
name: entry.name,
|
|
133
139
|
size_bytes: sizeBytes,
|
|
134
|
-
sha256,
|
|
135
|
-
...(previewText !== null && previewText !== ""
|
|
140
|
+
sha256: scanned.sha256,
|
|
141
|
+
...(scanned.previewText !== null && scanned.previewText !== ""
|
|
142
|
+
? { preview_text: scanned.previewText }
|
|
143
|
+
: {}),
|
|
136
144
|
});
|
|
137
145
|
}
|
|
138
146
|
}
|
|
139
147
|
await walk(workspaceDir, 0);
|
|
140
|
-
return {
|
|
148
|
+
return {
|
|
149
|
+
files,
|
|
150
|
+
truncated,
|
|
151
|
+
measurement: {
|
|
152
|
+
fileCount: observedFileCount,
|
|
153
|
+
totalBytes: observedTotalBytes,
|
|
154
|
+
durationMs: Math.max(0, performance.now() - scanStartedAt),
|
|
155
|
+
truncated,
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/** 捕获普通 Agent turn 开始前的 workspace 基线。 */
|
|
160
|
+
export async function captureWorkspaceFileBaseline(workspaceDir, limits = {}) {
|
|
161
|
+
const { files, truncated } = await scanWorkspaceFiles(workspaceDir, limits);
|
|
162
|
+
return {
|
|
163
|
+
files: new Map(files.map((file) => [
|
|
164
|
+
file.path,
|
|
165
|
+
{ sizeBytes: file.size_bytes ?? 0, sha256: file.sha256 ?? "" },
|
|
166
|
+
])),
|
|
167
|
+
truncated,
|
|
168
|
+
};
|
|
141
169
|
}
|
|
142
170
|
/**
|
|
143
171
|
* 扫描并逐个上报文件候选。单文件上报失败只 warn 不中断;返回成功上报数。
|
|
144
172
|
*/
|
|
145
|
-
export async function reportFileCandidates(client, agentRunId, workspaceDir, log, limits) {
|
|
146
|
-
const
|
|
173
|
+
export async function reportFileCandidates(client, agentRunId, workspaceDir, log, limits, baseline) {
|
|
174
|
+
const scan = await scanWorkspaceFiles(workspaceDir, limits);
|
|
175
|
+
const measurement = scan.measurement;
|
|
176
|
+
try {
|
|
177
|
+
await client.recordWorkspaceScanMeasurement?.(agentRunId, measurement);
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
log.warn("workspace scan measurement report failed", {
|
|
181
|
+
agentRunId,
|
|
182
|
+
error: err instanceof Error ? err.message : String(err),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
const files = baseline
|
|
186
|
+
? scan.files.flatMap((file) => {
|
|
187
|
+
const previous = baseline.files.get(file.path);
|
|
188
|
+
if (previous &&
|
|
189
|
+
previous.sizeBytes === file.size_bytes &&
|
|
190
|
+
previous.sha256 === file.sha256) {
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
if (!previous && baseline.truncated) {
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
return [{ ...file, event: previous ? "modified" : "created" }];
|
|
197
|
+
})
|
|
198
|
+
: scan.files;
|
|
199
|
+
const truncated = scan.truncated || Boolean(baseline?.truncated);
|
|
147
200
|
if (truncated) {
|
|
148
201
|
log.warn("workspace file scan truncated", { agentRunId, reported: files.length });
|
|
149
202
|
}
|
|
@@ -180,5 +233,5 @@ export async function reportFileCandidates(client, agentRunId, workspaceDir, log
|
|
|
180
233
|
clearTimeout(timeout);
|
|
181
234
|
}
|
|
182
235
|
}
|
|
183
|
-
return { reported, failed, truncated };
|
|
236
|
+
return { reported, failed, truncated, scan: measurement };
|
|
184
237
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,12 @@ export * from "./agent-service-ws-protocol.js";
|
|
|
11
11
|
export * from "./run-dispatcher.js";
|
|
12
12
|
export * from "./run-queue.js";
|
|
13
13
|
export * from "./workspace.js";
|
|
14
|
+
export * from "./workspace-entry-set.js";
|
|
15
|
+
export * from "./workspace-materialization.js";
|
|
16
|
+
export * from "./workspace-snapshot-staging.js";
|
|
17
|
+
export * from "./workspace-snapshot-policy.js";
|
|
18
|
+
export * from "./workspace-snapshot-control.js";
|
|
19
|
+
export * from "./workspace-restore.js";
|
|
14
20
|
export * from "./transcript.js";
|
|
15
21
|
export * from "./file-candidates.js";
|
|
16
22
|
export * from "./doctor.js";
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,12 @@ export * from "./agent-service-ws-protocol.js";
|
|
|
11
11
|
export * from "./run-dispatcher.js";
|
|
12
12
|
export * from "./run-queue.js";
|
|
13
13
|
export * from "./workspace.js";
|
|
14
|
+
export * from "./workspace-entry-set.js";
|
|
15
|
+
export * from "./workspace-materialization.js";
|
|
16
|
+
export * from "./workspace-snapshot-staging.js";
|
|
17
|
+
export * from "./workspace-snapshot-policy.js";
|
|
18
|
+
export * from "./workspace-snapshot-control.js";
|
|
19
|
+
export * from "./workspace-restore.js";
|
|
14
20
|
export * from "./transcript.js";
|
|
15
21
|
export * from "./file-candidates.js";
|
|
16
22
|
export * from "./doctor.js";
|
package/dist/run-dispatcher.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ScanLimits } from "./file-candidates.js";
|
|
1
|
+
import { type ScanLimits, type WorkspaceScanMeasurement } from "./file-candidates.js";
|
|
2
2
|
import { type InputAttachmentGrant } from "./input-attachments.js";
|
|
3
3
|
import { type Logger } from "./log.js";
|
|
4
4
|
import type { PreparedRuntimeSkillProvider } from "./runtime-skills.js";
|
|
@@ -23,11 +23,12 @@ export interface PreparedPersistentTurn {
|
|
|
23
23
|
export interface PersistentSessionExecution {
|
|
24
24
|
prepareTurn(payload: RunStartPayload): PreparedPersistentTurn;
|
|
25
25
|
persistNativeSession(sessionId: string): void;
|
|
26
|
-
finishTurn(payload: RunStartPayload): void
|
|
26
|
+
finishTurn(payload: RunStartPayload): Promise<void>;
|
|
27
27
|
}
|
|
28
28
|
export interface RunReportingClient {
|
|
29
29
|
postEvent(agentRunId: string, event: RunEvent): Promise<void | RunEventReceipt>;
|
|
30
30
|
postFile(agentRunId: string, file: import("./types.js").RunFileCandidate): Promise<unknown>;
|
|
31
|
+
recordWorkspaceScanMeasurement?(agentRunId: string, measurement: WorkspaceScanMeasurement): Promise<void> | void;
|
|
31
32
|
getRunRuntimeProfile?(agentRunId: string): Promise<CourseRuntimeProfile>;
|
|
32
33
|
}
|
|
33
34
|
/**
|
package/dist/run-dispatcher.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { CourseClientError, isRunTerminal } from "./course-client.js";
|
|
3
|
-
import { reportFileCandidates } from "./file-candidates.js";
|
|
3
|
+
import { captureWorkspaceFileBaseline, reportFileCandidates, } from "./file-candidates.js";
|
|
4
4
|
import { InputAttachmentMaterializationError, materializeInputAttachments, } from "./input-attachments.js";
|
|
5
5
|
import { log as defaultLog } from "./log.js";
|
|
6
6
|
import { MAX_PROGRESS_EVENTS_PER_ATTEMPT, tryNormalizeProgressReport, } from "./mcp/report-progress.js";
|
|
@@ -10,14 +10,17 @@ import { RunQueue } from "./run-queue.js";
|
|
|
10
10
|
import { applyRunRuntimeProfile, cleanupRunRuntimeProfile, RuntimeProfileApplyError, runtimeProfileInstructions, } from "./runtime-profile.js";
|
|
11
11
|
import { TranscriptWriter } from "./transcript.js";
|
|
12
12
|
import { buildToolObservation } from "./tool-observation.js";
|
|
13
|
+
import { buildReasoningTracePayload, extractReasoningText, REASONING_TRACE_MAX_CHARS, } from "./trace-projection.js";
|
|
13
14
|
import { RuntimeExecutionError, } from "./types.js";
|
|
14
15
|
import { ensureRunWorkspace, transcriptPath } from "./workspace.js";
|
|
16
|
+
import { isWorkspaceQuotaExceededError } from "./workspace-filesystem.js";
|
|
15
17
|
// 与 runtimes/index.ts 的 DEFAULT_RUNTIME_ID 保持一致(此处不 import registry,避免拉入全部 adapter)。
|
|
16
18
|
const DEFAULT_RUNTIME_ID = "codex";
|
|
17
19
|
const DEFAULT_TIMEOUT_SECONDS = 900;
|
|
18
20
|
const MIN_TIMEOUT_SECONDS = 30;
|
|
19
21
|
const MAX_TIMEOUT_SECONDS = 7200;
|
|
20
22
|
const DEFAULT_MAX_OUTPUT_CHARS = 100_000;
|
|
23
|
+
const DEFAULT_MAX_TOOL_CALLS = 100;
|
|
21
24
|
// wire 上单块文本上限;完整文本在本地 transcript。
|
|
22
25
|
const BLOCK_TEXT_MAX_CHARS = 4000;
|
|
23
26
|
const CONTENT_FLUSH_MAX_CHARS = 512;
|
|
@@ -458,6 +461,9 @@ export class RunDispatcher {
|
|
|
458
461
|
if (missingCapabilities.length > 0) {
|
|
459
462
|
throw new RuntimeExecutionError(`runtime is missing required capabilities: ${missingCapabilities.join(", ")}`, "runtime_unavailable");
|
|
460
463
|
}
|
|
464
|
+
// 迁移复制进来的文件属于 CourseRun workspace 基线,不属于本轮 TaskRun 产物。
|
|
465
|
+
// 模型执行后只上报相对这份快照新增或修改的文件。
|
|
466
|
+
const workspaceFileBaseline = await captureWorkspaceFileBaseline(workspaceDir, this.scanLimits);
|
|
461
467
|
const activeTranscript = new TranscriptWriter(persistentTurn?.transcriptFile ?? transcriptPath(runId));
|
|
462
468
|
transcript = activeTranscript;
|
|
463
469
|
const timeoutSeconds = clampTimeoutSeconds(payload.limits.timeout_seconds);
|
|
@@ -548,6 +554,52 @@ export class RunDispatcher {
|
|
|
548
554
|
},
|
|
549
555
|
});
|
|
550
556
|
};
|
|
557
|
+
// Reasoning text accumulates into one durable block-level run.trace per segment;
|
|
558
|
+
// realtime reasoning_delta blocks stream through the transient channel in parallel.
|
|
559
|
+
let reasoningBuffer = "";
|
|
560
|
+
let reasoningTruncated = false;
|
|
561
|
+
const collectReasoning = async (block) => {
|
|
562
|
+
if (block.kind === "progress")
|
|
563
|
+
return;
|
|
564
|
+
const thinkingText = block.kind === "thinking" && typeof block.text === "string" ? block.text : "";
|
|
565
|
+
const reasoningText = thinkingText + extractReasoningText(block.raw);
|
|
566
|
+
if (!reasoningText)
|
|
567
|
+
return;
|
|
568
|
+
if (reasoningBuffer.length >= REASONING_TRACE_MAX_CHARS) {
|
|
569
|
+
reasoningTruncated = true;
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
const room = REASONING_TRACE_MAX_CHARS - reasoningBuffer.length;
|
|
573
|
+
if (reasoningText.length > room) {
|
|
574
|
+
reasoningBuffer += reasoningText.slice(0, room);
|
|
575
|
+
reasoningTruncated = true;
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
reasoningBuffer += reasoningText;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
for (const text of chunkUnicodeText(redactSecretString(reasoningText), BLOCK_TEXT_MAX_CHARS)) {
|
|
582
|
+
if (!text)
|
|
583
|
+
continue;
|
|
584
|
+
await queueStreamEvent({
|
|
585
|
+
type: "run.block",
|
|
586
|
+
text,
|
|
587
|
+
payload: {
|
|
588
|
+
schema_version: AGENT_STREAM_SCHEMA_VERSION,
|
|
589
|
+
kind: "reasoning_delta",
|
|
590
|
+
runtime: runtime.id,
|
|
591
|
+
},
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
const flushReasoningTrace = async () => {
|
|
596
|
+
if (!reasoningBuffer)
|
|
597
|
+
return;
|
|
598
|
+
const payload = buildReasoningTracePayload(reasoningBuffer, runtime.id, reasoningTruncated);
|
|
599
|
+
reasoningBuffer = "";
|
|
600
|
+
reasoningTruncated = false;
|
|
601
|
+
await send({ type: "run.trace", payload });
|
|
602
|
+
};
|
|
551
603
|
const sink = {
|
|
552
604
|
skillEvent: async (event) => {
|
|
553
605
|
await flushContent();
|
|
@@ -653,7 +705,7 @@ export class RunDispatcher {
|
|
|
653
705
|
const configured = payload.limits.max_tool_calls;
|
|
654
706
|
const maxToolCalls = typeof configured === "number" && Number.isFinite(configured)
|
|
655
707
|
? Math.max(1, Math.min(1000, Math.floor(configured)))
|
|
656
|
-
:
|
|
708
|
+
: DEFAULT_MAX_TOOL_CALLS;
|
|
657
709
|
if (toolCalls > maxToolCalls) {
|
|
658
710
|
toolLimitExceeded = true;
|
|
659
711
|
controller.abort();
|
|
@@ -662,6 +714,7 @@ export class RunDispatcher {
|
|
|
662
714
|
}
|
|
663
715
|
if (serverTerminal)
|
|
664
716
|
return;
|
|
717
|
+
await collectReasoning(block);
|
|
665
718
|
if (block.kind === "text_delta") {
|
|
666
719
|
if (!block.text)
|
|
667
720
|
return;
|
|
@@ -677,7 +730,10 @@ export class RunDispatcher {
|
|
|
677
730
|
}
|
|
678
731
|
await flushContent();
|
|
679
732
|
if (block.kind === "thinking") {
|
|
680
|
-
|
|
733
|
+
const phase = block.phase ?? "in_progress";
|
|
734
|
+
await sendReasoningPhase(phase);
|
|
735
|
+
if (phase === "completed")
|
|
736
|
+
await flushReasoningTrace();
|
|
681
737
|
return;
|
|
682
738
|
}
|
|
683
739
|
if (block.kind === "status") {
|
|
@@ -698,6 +754,7 @@ export class RunDispatcher {
|
|
|
698
754
|
if (block.kind === "tool_call") {
|
|
699
755
|
lastReportedKind = block.kind;
|
|
700
756
|
lastReasoningPhase = null;
|
|
757
|
+
await flushReasoningTrace();
|
|
701
758
|
const observation = buildToolObservation(block, runtime.id, workspaceDir);
|
|
702
759
|
if (observation) {
|
|
703
760
|
await send({
|
|
@@ -806,6 +863,7 @@ export class RunDispatcher {
|
|
|
806
863
|
: {}),
|
|
807
864
|
}, sink, controller.signal);
|
|
808
865
|
await flushContent();
|
|
866
|
+
await flushReasoningTrace();
|
|
809
867
|
if (serverTerminal)
|
|
810
868
|
return;
|
|
811
869
|
if (controller.signal.aborted)
|
|
@@ -863,7 +921,7 @@ export class RunDispatcher {
|
|
|
863
921
|
activeTranscript.writeFinal(acceptedOutput);
|
|
864
922
|
fileReportStartedAt = this.now();
|
|
865
923
|
try {
|
|
866
|
-
await reportFileCandidates(this.client, runId, workspaceDir, this.log, this.scanLimits);
|
|
924
|
+
await reportFileCandidates(this.client, runId, workspaceDir, this.log, this.scanLimits, workspaceFileBaseline);
|
|
867
925
|
}
|
|
868
926
|
finally {
|
|
869
927
|
fileReportFinishedAt = this.now();
|
|
@@ -894,13 +952,15 @@ export class RunDispatcher {
|
|
|
894
952
|
}
|
|
895
953
|
else {
|
|
896
954
|
const info = errorInfo(err);
|
|
897
|
-
const errorType = err
|
|
898
|
-
?
|
|
899
|
-
: err instanceof
|
|
955
|
+
const errorType = isWorkspaceQuotaExceededError(err)
|
|
956
|
+
? "workspace_user_quota_exceeded"
|
|
957
|
+
: err instanceof RuntimeProfileApplyError
|
|
900
958
|
? err.code
|
|
901
|
-
: err instanceof
|
|
902
|
-
? err.
|
|
903
|
-
:
|
|
959
|
+
: err instanceof InputAttachmentMaterializationError
|
|
960
|
+
? err.code
|
|
961
|
+
: err instanceof RuntimeExecutionError
|
|
962
|
+
? err.errorType
|
|
963
|
+
: "runtime_error";
|
|
904
964
|
await sendFailure(errorType, info.error_message, err, err instanceof RuntimeProfileApplyError
|
|
905
965
|
? { code: err.code, profile_apply_status: "failed" }
|
|
906
966
|
: {});
|
|
@@ -922,7 +982,7 @@ export class RunDispatcher {
|
|
|
922
982
|
});
|
|
923
983
|
}
|
|
924
984
|
try {
|
|
925
|
-
this.persistentSession?.finishTurn(payload);
|
|
985
|
+
await this.persistentSession?.finishTurn(payload);
|
|
926
986
|
}
|
|
927
987
|
finally {
|
|
928
988
|
this.inflight.delete(runId);
|
package/dist/runtime-env.js
CHANGED
|
@@ -42,11 +42,15 @@ export function clearAgentServiceControlEnv(env = process.env) {
|
|
|
42
42
|
export function runtimeChildEnv(env = process.env) {
|
|
43
43
|
const childEnv = { ...env };
|
|
44
44
|
const runtimeHome = childEnv.BOTLEARN_RUNTIME_HOME;
|
|
45
|
+
const activationId = childEnv.BOTLEARN_AGENT_SERVICE_ACTIVATION_ID;
|
|
45
46
|
clearAgentServiceControlEnv(childEnv);
|
|
46
47
|
for (const key of AGENT_SERVICE_SUPERVISOR_ENV_KEYS)
|
|
47
48
|
delete childEnv[key];
|
|
48
49
|
if (runtimeHome)
|
|
49
50
|
childEnv.HOME = runtimeHome;
|
|
51
|
+
if (activationId && /^[A-Za-z0-9_-]{1,120}$/u.test(activationId)) {
|
|
52
|
+
childEnv.BOTLEARN_RUNTIME_ACTIVATION_SCOPE = activationId;
|
|
53
|
+
}
|
|
50
54
|
return childEnv;
|
|
51
55
|
}
|
|
52
56
|
/**
|
|
@@ -146,10 +150,6 @@ export function runtimeChildLaunch(binary, args, env = process.env, options = {}
|
|
|
146
150
|
"-n",
|
|
147
151
|
"-H",
|
|
148
152
|
"-E",
|
|
149
|
-
"-u",
|
|
150
|
-
runtimeUser,
|
|
151
|
-
"-g",
|
|
152
|
-
runtimeGroup,
|
|
153
153
|
"--",
|
|
154
154
|
MANAGED_RUNTIME_LAUNCHER,
|
|
155
155
|
binary,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface WorkspaceWriterFreezeProof {
|
|
2
|
+
readonly proofVersion: "workspace-writer-freeze-proof/1";
|
|
3
|
+
readonly sandboxId: string;
|
|
4
|
+
readonly sandboxGeneration: number;
|
|
5
|
+
readonly runtimeSessionId: string;
|
|
6
|
+
readonly checkpointId: string;
|
|
7
|
+
readonly allWritersFrozen: true;
|
|
8
|
+
}
|
|
9
|
+
export declare function freezeRuntimeWriters(): Promise<void>;
|
|
10
|
+
export declare function thawRuntimeWriters(): Promise<void>;
|
|
11
|
+
export declare function assertWorkspaceWriterFreezeSupported(): void;
|
|
12
|
+
export declare function quiesceRuntimeWriters(activationId: string | null): Promise<void>;
|
|
13
|
+
export declare function issueWorkspaceWriterFreezeProof(scope: {
|
|
14
|
+
sandboxId: string;
|
|
15
|
+
sandboxGeneration: number;
|
|
16
|
+
runtimeSessionId: string;
|
|
17
|
+
checkpointId: string;
|
|
18
|
+
}): WorkspaceWriterFreezeProof;
|
|
19
|
+
export declare function isWorkspaceWriterFreezeProof(value: object): value is WorkspaceWriterFreezeProof;
|