@botlearn-course/daemon 0.0.1 → 0.0.3
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 +12 -1
- package/dist/agent-service-session.d.ts +67 -0
- package/dist/agent-service-session.js +796 -0
- package/dist/agent-service-ws-protocol.d.ts +28 -0
- package/dist/agent-service-ws-protocol.js +128 -0
- package/dist/cli.d.ts +1 -2
- package/dist/cli.js +114 -13
- package/dist/course-client.js +4 -2
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/mcp/report-progress-server.d.ts +21 -0
- package/dist/mcp/report-progress-server.js +136 -0
- package/dist/mcp/report-progress.d.ts +29 -0
- package/dist/mcp/report-progress.js +60 -0
- package/dist/run-dispatcher.d.ts +15 -0
- package/dist/run-dispatcher.js +126 -6
- package/dist/runtime-env.d.ts +9 -0
- package/dist/runtime-env.js +40 -0
- package/dist/runtime-profile.js +25 -13
- package/dist/runtimes/acp-stream.js +2 -0
- package/dist/runtimes/codex.d.ts +1 -1
- package/dist/runtimes/codex.js +2 -2
- package/dist/runtimes/deepseek-tui.d.ts +6 -2
- package/dist/runtimes/deepseek-tui.js +238 -41
- package/dist/runtimes/engine.d.ts +14 -3
- package/dist/runtimes/engine.js +32 -5
- package/dist/runtimes/hermes-agent.d.ts +1 -1
- package/dist/runtimes/hermes-agent.js +3 -2
- package/dist/runtimes/ndjson-stream.d.ts +1 -1
- package/dist/runtimes/ndjson-stream.js +4 -2
- package/dist/runtimes/openclaw-acp.js +3 -1
- package/dist/runtimes/progress.d.ts +50 -0
- package/dist/runtimes/progress.js +339 -0
- package/dist/sandbox-supervisor.d.ts +3 -0
- package/dist/sandbox-supervisor.js +176 -0
- package/dist/transcript.js +6 -0
- package/dist/types.d.ts +29 -3
- package/dist/websocket-client.d.ts +43 -0
- package/dist/websocket-client.js +320 -0
- package/dist/workspace.d.ts +9 -0
- package/dist/workspace.js +43 -2
- package/package.json +3 -2
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export declare const AGENT_SERVICE_WS_SCHEMA: "botlearn-agent-sandbox-ws/0.1";
|
|
2
|
+
export declare const AGENT_SERVICE_WS_SUBPROTOCOL: "botlearn-agent-sandbox.v1";
|
|
3
|
+
export type SandboxFrameType = "session.hello" | "session.sync" | "turn.start" | "turn.cancel" | "session.drain" | "session.shutdown" | "event.ack" | "turn.file.upload_grant" | "auth.rotate" | "ping" | "session.ready" | "session.heartbeat" | "command.ack" | "turn.event" | "turn.file.prepare" | "turn.file.committed" | "session.drained" | "pong" | "protocol.error";
|
|
4
|
+
export interface SandboxFrame {
|
|
5
|
+
schema_version: typeof AGENT_SERVICE_WS_SCHEMA;
|
|
6
|
+
type: SandboxFrameType;
|
|
7
|
+
frame_id: string;
|
|
8
|
+
session_id: string;
|
|
9
|
+
session_generation: number;
|
|
10
|
+
connection_epoch: number;
|
|
11
|
+
seq: number;
|
|
12
|
+
sent_at: string;
|
|
13
|
+
agent_run_id: string | null;
|
|
14
|
+
worker_attempt: number | null;
|
|
15
|
+
payload: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
export declare function parseSandboxFrame(raw: string, maxBytes?: number): SandboxFrame;
|
|
18
|
+
export declare function createSandboxFrame(input: {
|
|
19
|
+
type: SandboxFrameType;
|
|
20
|
+
sessionId: string;
|
|
21
|
+
sessionGeneration: number;
|
|
22
|
+
connectionEpoch: number;
|
|
23
|
+
seq: number;
|
|
24
|
+
payload?: Record<string, unknown>;
|
|
25
|
+
agentRunId?: string;
|
|
26
|
+
workerAttempt?: number;
|
|
27
|
+
frameId?: string;
|
|
28
|
+
}): SandboxFrame;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
export const AGENT_SERVICE_WS_SCHEMA = "botlearn-agent-sandbox-ws/0.1";
|
|
3
|
+
export const AGENT_SERVICE_WS_SUBPROTOCOL = "botlearn-agent-sandbox.v1";
|
|
4
|
+
const FRAME_TYPES = new Set([
|
|
5
|
+
"session.hello",
|
|
6
|
+
"session.sync",
|
|
7
|
+
"turn.start",
|
|
8
|
+
"turn.cancel",
|
|
9
|
+
"session.drain",
|
|
10
|
+
"session.shutdown",
|
|
11
|
+
"event.ack",
|
|
12
|
+
"turn.file.upload_grant",
|
|
13
|
+
"auth.rotate",
|
|
14
|
+
"ping",
|
|
15
|
+
"session.ready",
|
|
16
|
+
"session.heartbeat",
|
|
17
|
+
"command.ack",
|
|
18
|
+
"turn.event",
|
|
19
|
+
"turn.file.prepare",
|
|
20
|
+
"turn.file.committed",
|
|
21
|
+
"session.drained",
|
|
22
|
+
"pong",
|
|
23
|
+
"protocol.error",
|
|
24
|
+
]);
|
|
25
|
+
const TURN_TYPES = new Set([
|
|
26
|
+
"turn.start",
|
|
27
|
+
"turn.cancel",
|
|
28
|
+
"event.ack",
|
|
29
|
+
"turn.file.upload_grant",
|
|
30
|
+
"turn.event",
|
|
31
|
+
"turn.file.prepare",
|
|
32
|
+
"turn.file.committed",
|
|
33
|
+
]);
|
|
34
|
+
const FRAME_KEYS = new Set([
|
|
35
|
+
"schema_version",
|
|
36
|
+
"type",
|
|
37
|
+
"frame_id",
|
|
38
|
+
"session_id",
|
|
39
|
+
"session_generation",
|
|
40
|
+
"connection_epoch",
|
|
41
|
+
"seq",
|
|
42
|
+
"sent_at",
|
|
43
|
+
"agent_run_id",
|
|
44
|
+
"worker_attempt",
|
|
45
|
+
"payload",
|
|
46
|
+
]);
|
|
47
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
48
|
+
function positiveInteger(value, label) {
|
|
49
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
50
|
+
throw new Error(`${label} must be a positive integer`);
|
|
51
|
+
}
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
export function parseSandboxFrame(raw, maxBytes = 262_144) {
|
|
55
|
+
if (Buffer.byteLength(raw, "utf8") > maxBytes)
|
|
56
|
+
throw new Error("frame exceeds size limit");
|
|
57
|
+
const decoded = JSON.parse(raw);
|
|
58
|
+
if (!decoded || typeof decoded !== "object" || Array.isArray(decoded)) {
|
|
59
|
+
throw new Error("frame must be an object");
|
|
60
|
+
}
|
|
61
|
+
const value = decoded;
|
|
62
|
+
for (const key of Object.keys(value)) {
|
|
63
|
+
if (!FRAME_KEYS.has(key))
|
|
64
|
+
throw new Error(`unknown sandbox WebSocket frame field: ${key}`);
|
|
65
|
+
}
|
|
66
|
+
if (value.schema_version !== AGENT_SERVICE_WS_SCHEMA) {
|
|
67
|
+
throw new Error("unsupported sandbox WebSocket schema");
|
|
68
|
+
}
|
|
69
|
+
if (typeof value.type !== "string" || !FRAME_TYPES.has(value.type)) {
|
|
70
|
+
throw new Error("unknown sandbox WebSocket frame type");
|
|
71
|
+
}
|
|
72
|
+
const type = value.type;
|
|
73
|
+
const agentRunId = typeof value.agent_run_id === "string" ? value.agent_run_id : null;
|
|
74
|
+
const workerAttempt = value.worker_attempt == null
|
|
75
|
+
? null
|
|
76
|
+
: positiveInteger(value.worker_attempt, "worker_attempt");
|
|
77
|
+
if (TURN_TYPES.has(type) && (!agentRunId || workerAttempt === null)) {
|
|
78
|
+
throw new Error(`${type} requires turn fencing fields`);
|
|
79
|
+
}
|
|
80
|
+
if (!TURN_TYPES.has(type) && (agentRunId !== null || workerAttempt !== null)) {
|
|
81
|
+
throw new Error(`${type} cannot carry turn fencing fields`);
|
|
82
|
+
}
|
|
83
|
+
if (!value.payload || typeof value.payload !== "object" || Array.isArray(value.payload)) {
|
|
84
|
+
throw new Error("frame payload must be an object");
|
|
85
|
+
}
|
|
86
|
+
for (const key of ["frame_id", "session_id", "sent_at"]) {
|
|
87
|
+
if (typeof value[key] !== "string" || !value[key])
|
|
88
|
+
throw new Error(`${key} is required`);
|
|
89
|
+
}
|
|
90
|
+
if (value.frame_id.length > 120)
|
|
91
|
+
throw new Error("frame_id is too long");
|
|
92
|
+
if (!UUID_PATTERN.test(value.session_id))
|
|
93
|
+
throw new Error("session_id must be a UUID");
|
|
94
|
+
if (agentRunId !== null && !UUID_PATTERN.test(agentRunId)) {
|
|
95
|
+
throw new Error("agent_run_id must be a UUID");
|
|
96
|
+
}
|
|
97
|
+
if (Number.isNaN(Date.parse(value.sent_at)))
|
|
98
|
+
throw new Error("sent_at is invalid");
|
|
99
|
+
return {
|
|
100
|
+
schema_version: AGENT_SERVICE_WS_SCHEMA,
|
|
101
|
+
type,
|
|
102
|
+
frame_id: value.frame_id,
|
|
103
|
+
session_id: value.session_id,
|
|
104
|
+
session_generation: positiveInteger(value.session_generation, "session_generation"),
|
|
105
|
+
connection_epoch: positiveInteger(value.connection_epoch, "connection_epoch"),
|
|
106
|
+
seq: positiveInteger(value.seq, "seq"),
|
|
107
|
+
sent_at: value.sent_at,
|
|
108
|
+
agent_run_id: agentRunId,
|
|
109
|
+
worker_attempt: workerAttempt,
|
|
110
|
+
payload: value.payload,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export function createSandboxFrame(input) {
|
|
114
|
+
const frame = {
|
|
115
|
+
schema_version: AGENT_SERVICE_WS_SCHEMA,
|
|
116
|
+
type: input.type,
|
|
117
|
+
frame_id: input.frameId ?? `frm_${randomUUID().replaceAll("-", "")}`,
|
|
118
|
+
session_id: input.sessionId,
|
|
119
|
+
session_generation: input.sessionGeneration,
|
|
120
|
+
connection_epoch: input.connectionEpoch,
|
|
121
|
+
seq: input.seq,
|
|
122
|
+
sent_at: new Date().toISOString(),
|
|
123
|
+
agent_run_id: input.agentRunId ?? null,
|
|
124
|
+
worker_attempt: input.workerAttempt ?? null,
|
|
125
|
+
payload: input.payload ?? {},
|
|
126
|
+
};
|
|
127
|
+
return parseSandboxFrame(JSON.stringify(frame));
|
|
128
|
+
}
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { type Logger } from "./log.js";
|
|
3
3
|
import type { RunStartPayload } from "./types.js";
|
|
4
|
-
|
|
5
|
-
export declare function clearAgentServiceControlEnv(env?: NodeJS.ProcessEnv): void;
|
|
4
|
+
export { clearAgentServiceControlEnv } from "./runtime-env.js";
|
|
6
5
|
export interface CliArgs {
|
|
7
6
|
positional: string[];
|
|
8
7
|
flags: Record<string, string | boolean>;
|
package/dist/cli.js
CHANGED
|
@@ -5,14 +5,27 @@ import { hostname } from "node:os";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { authFilePath, clearAuth, machineFingerprint, readAuth, writeAuth } from "./auth-store.js";
|
|
7
7
|
import { AgentServiceRunClient } from "./agent-service-client.js";
|
|
8
|
+
import { AgentServiceSessionClient } from "./agent-service-session.js";
|
|
8
9
|
import { CourseClient, isAuthFailure, loginCourseDaemon } from "./course-client.js";
|
|
9
10
|
import { runCourseDoctor } from "./doctor.js";
|
|
10
11
|
import { log } from "./log.js";
|
|
11
12
|
import { augmentProcessPath } from "./path-env.js";
|
|
12
13
|
import { redactSecretString } from "./redaction.js";
|
|
13
14
|
import { RunDispatcher } from "./run-dispatcher.js";
|
|
15
|
+
import { clearAgentServiceControlEnv } from "./runtime-env.js";
|
|
14
16
|
import { RUNTIME_MODULES, detectAvailableRuntimeIds, fakeRuntimeEnabled, } from "./runtimes/index.js";
|
|
15
17
|
const pkg = createRequire(import.meta.url)("../package.json");
|
|
18
|
+
const SESSION_BOOTSTRAP_MAX_BYTES = 64 * 1024;
|
|
19
|
+
const SESSION_RUNTIME_ENV_KEYS = new Set([
|
|
20
|
+
"OPENAI_API_KEY",
|
|
21
|
+
"OPENAI_BASE_URL",
|
|
22
|
+
"ANTHROPIC_API_KEY",
|
|
23
|
+
"ANTHROPIC_BASE_URL",
|
|
24
|
+
"DEEPSEEK_API_KEY",
|
|
25
|
+
"DEEPSEEK_BASE_URL",
|
|
26
|
+
"GEMINI_API_KEY",
|
|
27
|
+
"GEMINI_BASE_URL",
|
|
28
|
+
]);
|
|
16
29
|
const HELP = `botlearn-course-daemon ${pkg.version} — run BotLearn Course tasks on your own machine (BYOA)
|
|
17
30
|
|
|
18
31
|
Usage:
|
|
@@ -21,6 +34,7 @@ Usage:
|
|
|
21
34
|
botlearn-course-daemon course logout
|
|
22
35
|
botlearn-course-daemon course doctor
|
|
23
36
|
botlearn-course-daemon agent-service run
|
|
37
|
+
botlearn-course-daemon agent-service session
|
|
24
38
|
botlearn-course-daemon --help | --version
|
|
25
39
|
|
|
26
40
|
Commands:
|
|
@@ -41,22 +55,15 @@ Agent Service sandbox environment:
|
|
|
41
55
|
BOTLEARN_COURSE_API_URL
|
|
42
56
|
BOTLEARN_AGENT_SERVICE_RUN_ID
|
|
43
57
|
BOTLEARN_AGENT_SERVICE_RUN_TOKEN
|
|
44
|
-
BOTLEARN_AGENT_SERVICE_WORKER_ID
|
|
58
|
+
BOTLEARN_AGENT_SERVICE_WORKER_ID
|
|
59
|
+
BOTLEARN_AGENT_SERVICE_WS_URL
|
|
60
|
+
BOTLEARN_AGENT_SERVICE_RUNTIME_SESSION_ID
|
|
61
|
+
BOTLEARN_AGENT_SERVICE_SESSION_TOKEN`;
|
|
45
62
|
// ---------------------------------------------------------------
|
|
46
63
|
// flag parser(极简:--k v / --k=v / 布尔开关)
|
|
47
64
|
// ---------------------------------------------------------------
|
|
48
|
-
const BOOLEAN_FLAGS = new Set(["once", "help", "version"]);
|
|
49
|
-
|
|
50
|
-
"BOTLEARN_COURSE_API_URL",
|
|
51
|
-
"BOTLEARN_AGENT_SERVICE_RUN_ID",
|
|
52
|
-
"BOTLEARN_AGENT_SERVICE_RUN_TOKEN",
|
|
53
|
-
"BOTLEARN_AGENT_SERVICE_WORKER_ID",
|
|
54
|
-
];
|
|
55
|
-
/** Keep the run-scoped Course credential out of runtime subprocess environments. */
|
|
56
|
-
export function clearAgentServiceControlEnv(env = process.env) {
|
|
57
|
-
for (const key of AGENT_SERVICE_CONTROL_ENV_KEYS)
|
|
58
|
-
delete env[key];
|
|
59
|
-
}
|
|
65
|
+
const BOOLEAN_FLAGS = new Set(["once", "help", "version", "bootstrap-stdin"]);
|
|
66
|
+
export { clearAgentServiceControlEnv } from "./runtime-env.js";
|
|
60
67
|
export function parseCliArgs(argv) {
|
|
61
68
|
const positional = [];
|
|
62
69
|
const flags = {};
|
|
@@ -297,6 +304,97 @@ async function cmdAgentServiceRun() {
|
|
|
297
304
|
}
|
|
298
305
|
return 0;
|
|
299
306
|
}
|
|
307
|
+
async function readSessionBootstrapFromStdin() {
|
|
308
|
+
const chunks = [];
|
|
309
|
+
let size = 0;
|
|
310
|
+
for await (const chunk of process.stdin) {
|
|
311
|
+
const bytes = Buffer.from(chunk);
|
|
312
|
+
size += bytes.length;
|
|
313
|
+
if (size > SESSION_BOOTSTRAP_MAX_BYTES) {
|
|
314
|
+
throw new Error("Agent Service session bootstrap exceeds size limit");
|
|
315
|
+
}
|
|
316
|
+
chunks.push(bytes);
|
|
317
|
+
}
|
|
318
|
+
const raw = Buffer.concat(chunks);
|
|
319
|
+
try {
|
|
320
|
+
const parsed = JSON.parse(raw.toString("utf8"));
|
|
321
|
+
if (parsed.schemaVersion !== "agent-service-session-bootstrap/0.1") {
|
|
322
|
+
throw new Error("Agent Service session bootstrap schema is unsupported");
|
|
323
|
+
}
|
|
324
|
+
const runtimeEnv = {};
|
|
325
|
+
if (parsed.runtimeEnv !== undefined) {
|
|
326
|
+
if (!parsed.runtimeEnv ||
|
|
327
|
+
typeof parsed.runtimeEnv !== "object" ||
|
|
328
|
+
Array.isArray(parsed.runtimeEnv)) {
|
|
329
|
+
throw new Error("Agent Service session bootstrap runtimeEnv is invalid");
|
|
330
|
+
}
|
|
331
|
+
for (const [key, value] of Object.entries(parsed.runtimeEnv)) {
|
|
332
|
+
if (!SESSION_RUNTIME_ENV_KEYS.has(key) ||
|
|
333
|
+
typeof value !== "string" ||
|
|
334
|
+
value.length < 1 ||
|
|
335
|
+
value.length > 8192) {
|
|
336
|
+
throw new Error(`Agent Service session bootstrap runtime env is forbidden: ${key}`);
|
|
337
|
+
}
|
|
338
|
+
runtimeEnv[key] = value;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const wsUrl = parsed.wsUrl;
|
|
342
|
+
const runtimeSessionId = parsed.runtimeSessionId;
|
|
343
|
+
const sessionToken = parsed.sessionToken;
|
|
344
|
+
if (typeof wsUrl !== "string" ||
|
|
345
|
+
!/^wss?:\/\//.test(wsUrl) ||
|
|
346
|
+
typeof runtimeSessionId !== "string" ||
|
|
347
|
+
!runtimeSessionId ||
|
|
348
|
+
typeof sessionToken !== "string" ||
|
|
349
|
+
!sessionToken) {
|
|
350
|
+
throw new Error("Agent Service session bootstrap is incomplete");
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
wsUrl,
|
|
354
|
+
runtimeSessionId,
|
|
355
|
+
sessionToken,
|
|
356
|
+
...(Object.keys(runtimeEnv).length > 0 ? { runtimeEnv } : {}),
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
finally {
|
|
360
|
+
raw.fill(0);
|
|
361
|
+
for (const chunk of chunks)
|
|
362
|
+
chunk.fill(0);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
async function cmdAgentServiceSession(args) {
|
|
366
|
+
const bootstrap = args.flags["bootstrap-stdin"] === true
|
|
367
|
+
? await readSessionBootstrapFromStdin()
|
|
368
|
+
: {
|
|
369
|
+
wsUrl: process.env.BOTLEARN_AGENT_SERVICE_WS_URL,
|
|
370
|
+
runtimeSessionId: process.env.BOTLEARN_AGENT_SERVICE_RUNTIME_SESSION_ID,
|
|
371
|
+
sessionToken: process.env.BOTLEARN_AGENT_SERVICE_SESSION_TOKEN,
|
|
372
|
+
runtimeEnv: undefined,
|
|
373
|
+
};
|
|
374
|
+
const { wsUrl, runtimeSessionId, sessionToken } = bootstrap;
|
|
375
|
+
if (!wsUrl || !runtimeSessionId || !sessionToken) {
|
|
376
|
+
console.error("Agent Service sandbox session environment is incomplete");
|
|
377
|
+
return 1;
|
|
378
|
+
}
|
|
379
|
+
augmentProcessPath();
|
|
380
|
+
const runtimes = new Map();
|
|
381
|
+
for (const mod of RUNTIME_MODULES) {
|
|
382
|
+
if (mod.hidden && !fakeRuntimeEnabled())
|
|
383
|
+
continue;
|
|
384
|
+
runtimes.set(mod.id, mod.create());
|
|
385
|
+
}
|
|
386
|
+
const client = new AgentServiceSessionClient({
|
|
387
|
+
wsUrl,
|
|
388
|
+
sessionId: runtimeSessionId,
|
|
389
|
+
sessionToken,
|
|
390
|
+
...(bootstrap.runtimeEnv ? { runtimeEnv: bootstrap.runtimeEnv } : {}),
|
|
391
|
+
runtimes,
|
|
392
|
+
daemonVersion: pkg.version,
|
|
393
|
+
});
|
|
394
|
+
clearAgentServiceControlEnv();
|
|
395
|
+
await client.run();
|
|
396
|
+
return 0;
|
|
397
|
+
}
|
|
300
398
|
// ---------------------------------------------------------------
|
|
301
399
|
// 入口
|
|
302
400
|
// ---------------------------------------------------------------
|
|
@@ -329,6 +427,9 @@ export async function runCli(argv) {
|
|
|
329
427
|
if (command === "agent-service" && sub === "run") {
|
|
330
428
|
return cmdAgentServiceRun();
|
|
331
429
|
}
|
|
430
|
+
if (command === "agent-service" && sub === "session") {
|
|
431
|
+
return cmdAgentServiceSession(args);
|
|
432
|
+
}
|
|
332
433
|
console.error(HELP);
|
|
333
434
|
return 1;
|
|
334
435
|
}
|
package/dist/course-client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { redactSecretString } from "./redaction.js";
|
|
1
|
+
import { redactSecretString, redactSecretsDeep } from "./redaction.js";
|
|
2
2
|
export class CourseClientError extends Error {
|
|
3
3
|
status;
|
|
4
4
|
constructor(status, message) {
|
|
@@ -100,7 +100,9 @@ export class CourseClient {
|
|
|
100
100
|
return this.request("GET", "/api/v1/daemon/runs/next");
|
|
101
101
|
}
|
|
102
102
|
async postEvent(agentRunId, event) {
|
|
103
|
-
|
|
103
|
+
const credentials = [this.accessToken, this.refreshToken].filter((value) => typeof value === "string");
|
|
104
|
+
const sanitized = redactSecretsDeep(event, 8, credentials);
|
|
105
|
+
await this.request("POST", `/api/v1/daemon/runs/${agentRunId}/events`, sanitized);
|
|
104
106
|
}
|
|
105
107
|
async postFile(agentRunId, file) {
|
|
106
108
|
return this.request("POST", `/api/v1/daemon/runs/${agentRunId}/files`, file);
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export * from "./types.js";
|
|
|
6
6
|
export * from "./auth-store.js";
|
|
7
7
|
export * from "./course-client.js";
|
|
8
8
|
export * from "./agent-service-client.js";
|
|
9
|
+
export * from "./agent-service-session.js";
|
|
10
|
+
export * from "./agent-service-ws-protocol.js";
|
|
9
11
|
export * from "./run-dispatcher.js";
|
|
10
12
|
export * from "./run-queue.js";
|
|
11
13
|
export * from "./workspace.js";
|
|
@@ -15,5 +17,7 @@ export * from "./doctor.js";
|
|
|
15
17
|
export * from "./log.js";
|
|
16
18
|
export * from "./redaction.js";
|
|
17
19
|
export * from "./runtime-profile.js";
|
|
20
|
+
export * from "./runtime-env.js";
|
|
21
|
+
export * from "./mcp/report-progress.js";
|
|
18
22
|
export * from "./runtimes/index.js";
|
|
19
23
|
export * from "./runtimes/engine.js";
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,8 @@ export * from "./types.js";
|
|
|
6
6
|
export * from "./auth-store.js";
|
|
7
7
|
export * from "./course-client.js";
|
|
8
8
|
export * from "./agent-service-client.js";
|
|
9
|
+
export * from "./agent-service-session.js";
|
|
10
|
+
export * from "./agent-service-ws-protocol.js";
|
|
9
11
|
export * from "./run-dispatcher.js";
|
|
10
12
|
export * from "./run-queue.js";
|
|
11
13
|
export * from "./workspace.js";
|
|
@@ -15,5 +17,7 @@ export * from "./doctor.js";
|
|
|
15
17
|
export * from "./log.js";
|
|
16
18
|
export * from "./redaction.js";
|
|
17
19
|
export * from "./runtime-profile.js";
|
|
20
|
+
export * from "./runtime-env.js";
|
|
21
|
+
export * from "./mcp/report-progress.js";
|
|
18
22
|
export * from "./runtimes/index.js";
|
|
19
23
|
export * from "./runtimes/engine.js";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
type JsonRpcId = string | number | null;
|
|
3
|
+
interface JsonRpcRequest {
|
|
4
|
+
jsonrpc?: unknown;
|
|
5
|
+
id?: unknown;
|
|
6
|
+
method?: unknown;
|
|
7
|
+
params?: unknown;
|
|
8
|
+
}
|
|
9
|
+
interface JsonRpcResponse {
|
|
10
|
+
jsonrpc: "2.0";
|
|
11
|
+
id: JsonRpcId;
|
|
12
|
+
result?: unknown;
|
|
13
|
+
error?: {
|
|
14
|
+
code: number;
|
|
15
|
+
message: string;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** Pure JSON-RPC request handler used by both stdio and deterministic unit tests. */
|
|
19
|
+
export declare function handleReportProgressRequest(request: JsonRpcRequest): JsonRpcResponse | null;
|
|
20
|
+
export declare function runReportProgressStdioServer(): void;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { createInterface } from "node:readline";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { normalizeProgressReport, ProgressValidationError, REPORT_PROGRESS_INPUT_SCHEMA, } from "./report-progress.js";
|
|
6
|
+
const DEFAULT_PROTOCOL_VERSION = "2024-11-05";
|
|
7
|
+
const TOOL = {
|
|
8
|
+
name: "report_progress",
|
|
9
|
+
description: "Report a concise, learner-visible execution phase update. This does not complete course progress.",
|
|
10
|
+
inputSchema: REPORT_PROGRESS_INPUT_SCHEMA,
|
|
11
|
+
};
|
|
12
|
+
function responseId(value) {
|
|
13
|
+
return typeof value === "string" || typeof value === "number" || value === null ? value : null;
|
|
14
|
+
}
|
|
15
|
+
function toolError(code, message) {
|
|
16
|
+
return {
|
|
17
|
+
content: [
|
|
18
|
+
{
|
|
19
|
+
type: "text",
|
|
20
|
+
text: JSON.stringify({
|
|
21
|
+
schemaVersion: "tool-result/0.1",
|
|
22
|
+
ok: false,
|
|
23
|
+
code,
|
|
24
|
+
error: { message },
|
|
25
|
+
evidence: [],
|
|
26
|
+
}),
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
isError: true,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** Pure JSON-RPC request handler used by both stdio and deterministic unit tests. */
|
|
33
|
+
export function handleReportProgressRequest(request) {
|
|
34
|
+
if (request.id === undefined)
|
|
35
|
+
return null;
|
|
36
|
+
const id = responseId(request.id);
|
|
37
|
+
if (request.jsonrpc !== "2.0" || typeof request.method !== "string") {
|
|
38
|
+
return { jsonrpc: "2.0", id, error: { code: -32600, message: "Invalid Request" } };
|
|
39
|
+
}
|
|
40
|
+
if (request.method === "initialize") {
|
|
41
|
+
const requested = request.params && typeof request.params === "object"
|
|
42
|
+
? request.params.protocolVersion
|
|
43
|
+
: undefined;
|
|
44
|
+
return {
|
|
45
|
+
jsonrpc: "2.0",
|
|
46
|
+
id,
|
|
47
|
+
result: {
|
|
48
|
+
protocolVersion: typeof requested === "string" && requested.length > 0
|
|
49
|
+
? requested
|
|
50
|
+
: DEFAULT_PROTOCOL_VERSION,
|
|
51
|
+
capabilities: { tools: {} },
|
|
52
|
+
serverInfo: { name: "botlearn-report-progress", version: "0.1.0" },
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (request.method === "tools/list") {
|
|
57
|
+
return { jsonrpc: "2.0", id, result: { tools: [TOOL] } };
|
|
58
|
+
}
|
|
59
|
+
if (request.method === "tools/call") {
|
|
60
|
+
const params = request.params && typeof request.params === "object"
|
|
61
|
+
? request.params
|
|
62
|
+
: {};
|
|
63
|
+
if (params.name !== TOOL.name) {
|
|
64
|
+
return {
|
|
65
|
+
jsonrpc: "2.0",
|
|
66
|
+
id,
|
|
67
|
+
result: toolError("unknown_tool", "Unknown tool"),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const progress = normalizeProgressReport(params.arguments);
|
|
72
|
+
return {
|
|
73
|
+
jsonrpc: "2.0",
|
|
74
|
+
id,
|
|
75
|
+
result: {
|
|
76
|
+
content: [
|
|
77
|
+
{
|
|
78
|
+
type: "text",
|
|
79
|
+
text: JSON.stringify({
|
|
80
|
+
schemaVersion: "tool-result/0.1",
|
|
81
|
+
ok: true,
|
|
82
|
+
code: "progress_reported",
|
|
83
|
+
data: progress,
|
|
84
|
+
evidence: [],
|
|
85
|
+
}),
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
isError: false,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (!(error instanceof ProgressValidationError))
|
|
94
|
+
throw error;
|
|
95
|
+
return {
|
|
96
|
+
jsonrpc: "2.0",
|
|
97
|
+
id,
|
|
98
|
+
result: toolError("invalid_progress", error.message),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return { jsonrpc: "2.0", id, error: { code: -32601, message: "Method not found" } };
|
|
103
|
+
}
|
|
104
|
+
export function runReportProgressStdioServer() {
|
|
105
|
+
const lines = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
106
|
+
lines.on("line", (line) => {
|
|
107
|
+
if (!line.trim())
|
|
108
|
+
return;
|
|
109
|
+
let response;
|
|
110
|
+
try {
|
|
111
|
+
response = handleReportProgressRequest(JSON.parse(line));
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
response = {
|
|
115
|
+
jsonrpc: "2.0",
|
|
116
|
+
id: null,
|
|
117
|
+
error: { code: -32700, message: "Parse error" },
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (response)
|
|
121
|
+
process.stdout.write(`${JSON.stringify(response)}\n`);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
function isMainModule() {
|
|
125
|
+
const entry = process.argv[1];
|
|
126
|
+
if (!entry)
|
|
127
|
+
return false;
|
|
128
|
+
try {
|
|
129
|
+
return realpathSync(entry) === fileURLToPath(import.meta.url);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (isMainModule())
|
|
136
|
+
runReportProgressStdioServer();
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare const PROGRESS_STATUSES: readonly ["in_progress", "completed"];
|
|
2
|
+
export type ProgressStatus = (typeof PROGRESS_STATUSES)[number];
|
|
3
|
+
export interface ProgressReport {
|
|
4
|
+
summary: string;
|
|
5
|
+
status: ProgressStatus;
|
|
6
|
+
}
|
|
7
|
+
export declare const MAX_PROGRESS_EVENTS_PER_ATTEMPT = 64;
|
|
8
|
+
export declare const REPORT_PROGRESS_INPUT_SCHEMA: {
|
|
9
|
+
readonly type: "object";
|
|
10
|
+
readonly additionalProperties: false;
|
|
11
|
+
readonly required: readonly ["summary", "status"];
|
|
12
|
+
readonly properties: {
|
|
13
|
+
readonly summary: {
|
|
14
|
+
readonly type: "string";
|
|
15
|
+
readonly minLength: 1;
|
|
16
|
+
readonly maxLength: 240;
|
|
17
|
+
};
|
|
18
|
+
readonly status: {
|
|
19
|
+
readonly type: "string";
|
|
20
|
+
readonly enum: readonly ["in_progress", "completed"];
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
export declare class ProgressValidationError extends Error {
|
|
25
|
+
constructor(message: string);
|
|
26
|
+
}
|
|
27
|
+
/** Strict shared normalizer: Unicode trim, exact fields/types, then code-point length. */
|
|
28
|
+
export declare function normalizeProgressReport(value: unknown): ProgressReport;
|
|
29
|
+
export declare function tryNormalizeProgressReport(value: unknown): ProgressReport | null;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export const PROGRESS_STATUSES = ["in_progress", "completed"];
|
|
2
|
+
export const MAX_PROGRESS_EVENTS_PER_ATTEMPT = 64;
|
|
3
|
+
export const REPORT_PROGRESS_INPUT_SCHEMA = {
|
|
4
|
+
type: "object",
|
|
5
|
+
additionalProperties: false,
|
|
6
|
+
required: ["summary", "status"],
|
|
7
|
+
properties: {
|
|
8
|
+
summary: {
|
|
9
|
+
type: "string",
|
|
10
|
+
minLength: 1,
|
|
11
|
+
maxLength: 240,
|
|
12
|
+
},
|
|
13
|
+
status: {
|
|
14
|
+
type: "string",
|
|
15
|
+
enum: PROGRESS_STATUSES,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
// Unicode White_Space plus BOM/ZWNBSP, matching the platform's user-facing trim behavior.
|
|
20
|
+
const UNICODE_WHITESPACE_AT_EDGES = /^(?:\p{White_Space}|\uFEFF)+|(?:\p{White_Space}|\uFEFF)+$/gu;
|
|
21
|
+
const MAX_PROGRESS_CODE_POINTS = 240;
|
|
22
|
+
export class ProgressValidationError extends Error {
|
|
23
|
+
constructor(message) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "ProgressValidationError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Strict shared normalizer: Unicode trim, exact fields/types, then code-point length. */
|
|
29
|
+
export function normalizeProgressReport(value) {
|
|
30
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
31
|
+
throw new ProgressValidationError("progress arguments must be an object");
|
|
32
|
+
}
|
|
33
|
+
const record = value;
|
|
34
|
+
const keys = Object.keys(record).sort();
|
|
35
|
+
if (keys.length !== 2 || keys[0] !== "status" || keys[1] !== "summary") {
|
|
36
|
+
throw new ProgressValidationError("progress arguments must contain only summary and status");
|
|
37
|
+
}
|
|
38
|
+
if (typeof record.summary !== "string") {
|
|
39
|
+
throw new ProgressValidationError("progress summary must be a string");
|
|
40
|
+
}
|
|
41
|
+
if (record.status !== "in_progress" && record.status !== "completed") {
|
|
42
|
+
throw new ProgressValidationError("progress status is invalid");
|
|
43
|
+
}
|
|
44
|
+
const summary = record.summary.replace(UNICODE_WHITESPACE_AT_EDGES, "");
|
|
45
|
+
const length = Array.from(summary).length;
|
|
46
|
+
if (length < 1 || length > MAX_PROGRESS_CODE_POINTS) {
|
|
47
|
+
throw new ProgressValidationError("progress summary must contain 1 to 240 Unicode code points");
|
|
48
|
+
}
|
|
49
|
+
return { summary, status: record.status };
|
|
50
|
+
}
|
|
51
|
+
export function tryNormalizeProgressReport(value) {
|
|
52
|
+
try {
|
|
53
|
+
return normalizeProgressReport(value);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (error instanceof ProgressValidationError)
|
|
57
|
+
return null;
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
package/dist/run-dispatcher.d.ts
CHANGED
|
@@ -6,6 +6,18 @@ export interface RunDispatcherOptions {
|
|
|
6
6
|
log?: Logger;
|
|
7
7
|
scanLimits?: ScanLimits;
|
|
8
8
|
now?: () => number;
|
|
9
|
+
persistentSession?: PersistentSessionExecution;
|
|
10
|
+
}
|
|
11
|
+
export interface PreparedPersistentTurn {
|
|
12
|
+
workspaceDir: string;
|
|
13
|
+
transcriptFile: string;
|
|
14
|
+
nativeSessionId: string | null;
|
|
15
|
+
contextRevision: number;
|
|
16
|
+
runtimeEnv?: NodeJS.ProcessEnv;
|
|
17
|
+
}
|
|
18
|
+
export interface PersistentSessionExecution {
|
|
19
|
+
prepareTurn(payload: RunStartPayload): PreparedPersistentTurn;
|
|
20
|
+
persistNativeSession(sessionId: string): void;
|
|
9
21
|
}
|
|
10
22
|
export interface RunReportingClient {
|
|
11
23
|
postEvent(agentRunId: string, event: RunEvent): Promise<void>;
|
|
@@ -27,10 +39,13 @@ export declare class RunDispatcher {
|
|
|
27
39
|
private readonly runtimes;
|
|
28
40
|
private readonly queue;
|
|
29
41
|
private readonly inflight;
|
|
42
|
+
private readonly scheduledRunIds;
|
|
43
|
+
private readonly pendingCancellations;
|
|
30
44
|
private readonly defaultRuntimeId;
|
|
31
45
|
private readonly log;
|
|
32
46
|
private readonly scanLimits?;
|
|
33
47
|
private readonly now;
|
|
48
|
+
private readonly persistentSession?;
|
|
34
49
|
constructor(client: RunReportingClient, runtimes: Map<string, CourseRuntime>, opts?: RunDispatcherOptions);
|
|
35
50
|
/** 排队执行一个 run。返回的 Promise 不 reject(失败已归一化回报为 run.failed)。 */
|
|
36
51
|
dispatch(payload: RunStartPayload): Promise<void>;
|