@botlearn-course/daemon 0.0.5 → 0.0.7
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 +6 -1
- package/dist/agent-service-session.js +4 -1
- package/dist/run-dispatcher.js +173 -11
- package/dist/runtime-env.js +5 -1
- package/dist/runtimes/claude-code.js +14 -3
- package/dist/runtimes/codex.js +17 -1
- package/dist/runtimes/deepseek-tui.js +54 -8
- package/dist/runtimes/engine.d.ts +5 -0
- package/dist/runtimes/engine.js +19 -2
- package/dist/runtimes/gemini.js +14 -1
- package/dist/runtimes/hermes-agent.js +19 -2
- package/dist/runtimes/kimi.js +9 -1
- package/dist/runtimes/openclaw-acp.js +21 -2
- package/dist/runtimes/progress.d.ts +13 -1
- package/dist/runtimes/progress.js +61 -6
- package/dist/sandbox-supervisor.js +22 -4
- package/dist/types.d.ts +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,7 +40,9 @@ npx @botlearn-course/daemon@latest course logout
|
|
|
40
40
|
`botlearn-sandbox-supervisor agent-service session`。它不是 BYOA 用户入口:生产环境只允许由
|
|
41
41
|
Agent Service 以固定 argv 启动,通过 stdin 接收一次性 bootstrap,并把 daemon/runtime 分别降权
|
|
42
42
|
到 `botlearn-control`/`user` UID。E2B Template 通过 root-owned 固定 launcher 将 DeepSeek
|
|
43
|
-
单向降权为 `user` 并设置 `no_new_privs`;runtime 用户本身没有 sudo
|
|
43
|
+
单向降权为 `user` 并设置 `no_new_privs`;runtime 用户本身没有 sudo 权限。托管启动链只执行
|
|
44
|
+
`/opt` 下的固定 Node、supervisor、daemon、launcher 与 DeepSeek 文件,不信任 E2B 会开放给 runtime
|
|
45
|
+
写入的 `/usr/local/bin`。
|
|
44
46
|
`agent-service session --bootstrap-stdin` 同样属于受 supervisor 保护的内部协议,不应直接暴露给
|
|
45
47
|
课程任务或 workspace shell。
|
|
46
48
|
|
|
@@ -103,6 +105,9 @@ runtime 凭据。
|
|
|
103
105
|
(SIGTERM → SIGKILL)。
|
|
104
106
|
- `report_progress` MCP 子进程以 clean environment 启动,不继承 Course 控制字段或模型凭据;
|
|
105
107
|
transcript 与 Course event 只保留严格归一化后的 `summary` / `status`,不保存原始 tool arguments。
|
|
108
|
+
- 浏览器可见执行流使用 `agent-stream/0.1`:assistant 正文按 100ms/512 字符合并上报;
|
|
109
|
+
reasoning 只报告阶段,工具只报告安全名称和生命周期。raw chain-of-thought、工具参数、工具结果
|
|
110
|
+
和完整命令输出不会进入 Course event。
|
|
106
111
|
- `course logout` 只删除本机凭据;要让服务端立即吊销该 daemon,请在前端 daemon 列表执行
|
|
107
112
|
revoke。
|
|
108
113
|
- 本包 production 依赖为零,发布 tarball 只包含 `dist/`、`README.md`、`package.json`、
|
|
@@ -94,7 +94,10 @@ function loadState(sessionId, token) {
|
|
|
94
94
|
return {
|
|
95
95
|
sessionGeneration: Number(value.sessionGeneration ?? 0),
|
|
96
96
|
nextOutboundSeq: Number(value.nextOutboundSeq ?? 0),
|
|
97
|
-
|
|
97
|
+
// A controller launch always supplies a freshly minted, session-scoped bootstrap
|
|
98
|
+
// token. Preserve runtime/command continuity from disk, but never let an expired
|
|
99
|
+
// persisted reconnect token shadow the controller's recovery credential.
|
|
100
|
+
reconnectToken: token,
|
|
98
101
|
completedCommands: Array.isArray(value.completedCommands)
|
|
99
102
|
? value.completedCommands.filter((item) => typeof item === "string")
|
|
100
103
|
: [],
|
package/dist/run-dispatcher.js
CHANGED
|
@@ -18,10 +18,28 @@ const MAX_TIMEOUT_SECONDS = 7200;
|
|
|
18
18
|
const DEFAULT_MAX_OUTPUT_CHARS = 100_000;
|
|
19
19
|
// wire 上单块文本上限;完整文本在本地 transcript。
|
|
20
20
|
const BLOCK_TEXT_MAX_CHARS = 4000;
|
|
21
|
+
const CONTENT_FLUSH_MAX_CHARS = 512;
|
|
22
|
+
const CONTENT_FLUSH_INTERVAL_MS = 100;
|
|
23
|
+
const AGENT_STREAM_SCHEMA_VERSION = "agent-stream/0.1";
|
|
24
|
+
const SAFE_TOOL_NAME = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,79}$/;
|
|
21
25
|
function clampTimeoutSeconds(value) {
|
|
22
26
|
const n = typeof value === "number" && Number.isFinite(value) ? value : DEFAULT_TIMEOUT_SECONDS;
|
|
23
27
|
return Math.min(MAX_TIMEOUT_SECONDS, Math.max(MIN_TIMEOUT_SECONDS, n));
|
|
24
28
|
}
|
|
29
|
+
function normalizeToolName(value) {
|
|
30
|
+
if (typeof value !== "string")
|
|
31
|
+
return undefined;
|
|
32
|
+
const name = value.trim();
|
|
33
|
+
return SAFE_TOOL_NAME.test(name) ? name : undefined;
|
|
34
|
+
}
|
|
35
|
+
function chunkUnicodeText(value, maxCodePoints) {
|
|
36
|
+
const codePoints = Array.from(value);
|
|
37
|
+
const chunks = [];
|
|
38
|
+
for (let offset = 0; offset < codePoints.length; offset += maxCodePoints) {
|
|
39
|
+
chunks.push(codePoints.slice(offset, offset + maxCodePoints).join(""));
|
|
40
|
+
}
|
|
41
|
+
return chunks;
|
|
42
|
+
}
|
|
25
43
|
/**
|
|
26
44
|
* Run dispatcher:把 Course Service 下发的 run.start 交给 runtime,
|
|
27
45
|
* 并把 runtime 输出归一化成 run.block / run.message / run.completed 回报 Course Service。
|
|
@@ -110,6 +128,7 @@ export class RunDispatcher {
|
|
|
110
128
|
let terminalSent = false;
|
|
111
129
|
let timedOut = false;
|
|
112
130
|
let timer;
|
|
131
|
+
let contentFlushTimer;
|
|
113
132
|
let profileApplied = false;
|
|
114
133
|
let toolCalls = 0;
|
|
115
134
|
let toolLimitExceeded = false;
|
|
@@ -265,8 +284,80 @@ export class RunDispatcher {
|
|
|
265
284
|
controller.abort();
|
|
266
285
|
}, timeoutSeconds * 1000);
|
|
267
286
|
let finalText = "";
|
|
268
|
-
// 上一次真正上了 wire 的块 kind:
|
|
287
|
+
// 上一次真正上了 wire 的块 kind:status 只在 kind 切换时上报一次,避免刷屏。
|
|
269
288
|
let lastReportedKind = null;
|
|
289
|
+
let lastReasoningPhase = null;
|
|
290
|
+
let pendingContent = "";
|
|
291
|
+
let streamEventFailure;
|
|
292
|
+
let streamEventChain = Promise.resolve();
|
|
293
|
+
const queueStreamEvent = (event) => {
|
|
294
|
+
const operation = streamEventChain.then(async () => {
|
|
295
|
+
if (streamEventFailure)
|
|
296
|
+
throw streamEventFailure;
|
|
297
|
+
await send(event);
|
|
298
|
+
});
|
|
299
|
+
streamEventChain = operation.catch((error) => {
|
|
300
|
+
streamEventFailure ??= error;
|
|
301
|
+
});
|
|
302
|
+
return operation;
|
|
303
|
+
};
|
|
304
|
+
const awaitStreamEvents = async () => {
|
|
305
|
+
await streamEventChain;
|
|
306
|
+
if (streamEventFailure)
|
|
307
|
+
throw streamEventFailure;
|
|
308
|
+
};
|
|
309
|
+
const flushContent = async () => {
|
|
310
|
+
if (contentFlushTimer !== undefined) {
|
|
311
|
+
clearTimeout(contentFlushTimer);
|
|
312
|
+
contentFlushTimer = undefined;
|
|
313
|
+
}
|
|
314
|
+
if (!pendingContent) {
|
|
315
|
+
await awaitStreamEvents();
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const content = pendingContent;
|
|
319
|
+
pendingContent = "";
|
|
320
|
+
for (const text of chunkUnicodeText(redactSecretString(content), BLOCK_TEXT_MAX_CHARS)) {
|
|
321
|
+
if (!text)
|
|
322
|
+
continue;
|
|
323
|
+
await queueStreamEvent({
|
|
324
|
+
type: "run.block",
|
|
325
|
+
text,
|
|
326
|
+
payload: {
|
|
327
|
+
schema_version: AGENT_STREAM_SCHEMA_VERSION,
|
|
328
|
+
kind: "text_delta",
|
|
329
|
+
runtime: runtime.id,
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
await awaitStreamEvents();
|
|
334
|
+
};
|
|
335
|
+
const scheduleContentFlush = () => {
|
|
336
|
+
if (contentFlushTimer !== undefined)
|
|
337
|
+
return;
|
|
338
|
+
contentFlushTimer = setTimeout(() => {
|
|
339
|
+
contentFlushTimer = undefined;
|
|
340
|
+
void flushContent().catch((error) => {
|
|
341
|
+
streamEventFailure ??= error;
|
|
342
|
+
});
|
|
343
|
+
}, CONTENT_FLUSH_INTERVAL_MS);
|
|
344
|
+
};
|
|
345
|
+
const sendReasoningPhase = async (phase) => {
|
|
346
|
+
if (phase === "completed" && lastReasoningPhase !== "in_progress")
|
|
347
|
+
return;
|
|
348
|
+
if (phase === lastReasoningPhase)
|
|
349
|
+
return;
|
|
350
|
+
lastReasoningPhase = phase;
|
|
351
|
+
await queueStreamEvent({
|
|
352
|
+
type: "run.block",
|
|
353
|
+
payload: {
|
|
354
|
+
schema_version: AGENT_STREAM_SCHEMA_VERSION,
|
|
355
|
+
kind: "reasoning",
|
|
356
|
+
runtime: runtime.id,
|
|
357
|
+
phase,
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
};
|
|
270
361
|
const sink = {
|
|
271
362
|
progressDispositions: async (dispositions) => {
|
|
272
363
|
progressDisposition.invalid += dispositions.invalid;
|
|
@@ -287,6 +378,7 @@ export class RunDispatcher {
|
|
|
287
378
|
},
|
|
288
379
|
block: async (block) => {
|
|
289
380
|
if (block.kind === "progress") {
|
|
381
|
+
await flushContent();
|
|
290
382
|
const normalized = tryNormalizeProgressReport({
|
|
291
383
|
summary: block.summary,
|
|
292
384
|
status: block.status,
|
|
@@ -363,19 +455,87 @@ export class RunDispatcher {
|
|
|
363
455
|
}
|
|
364
456
|
if (serverTerminal)
|
|
365
457
|
return;
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
458
|
+
if (block.kind === "text_delta") {
|
|
459
|
+
if (!block.text)
|
|
460
|
+
return;
|
|
461
|
+
pendingContent += block.text;
|
|
462
|
+
if (pendingContent.length >= CONTENT_FLUSH_MAX_CHARS) {
|
|
463
|
+
await flushContent();
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
scheduleContentFlush();
|
|
467
|
+
}
|
|
370
468
|
return;
|
|
371
|
-
|
|
372
|
-
await
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
}
|
|
469
|
+
}
|
|
470
|
+
await flushContent();
|
|
471
|
+
if (block.kind === "thinking") {
|
|
472
|
+
await sendReasoningPhase(block.phase ?? "in_progress");
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
if (block.kind === "status") {
|
|
476
|
+
if (lastReportedKind === "status")
|
|
477
|
+
return;
|
|
478
|
+
lastReportedKind = "status";
|
|
479
|
+
await queueStreamEvent({
|
|
480
|
+
type: "run.block",
|
|
481
|
+
payload: {
|
|
482
|
+
schema_version: AGENT_STREAM_SCHEMA_VERSION,
|
|
483
|
+
kind: "status",
|
|
484
|
+
runtime: runtime.id,
|
|
485
|
+
phase: "executing",
|
|
486
|
+
},
|
|
487
|
+
});
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
if (block.kind === "tool_call") {
|
|
491
|
+
lastReportedKind = block.kind;
|
|
492
|
+
lastReasoningPhase = null;
|
|
493
|
+
await queueStreamEvent({
|
|
494
|
+
type: "run.block",
|
|
495
|
+
payload: {
|
|
496
|
+
schema_version: AGENT_STREAM_SCHEMA_VERSION,
|
|
497
|
+
kind: "tool_call",
|
|
498
|
+
runtime: runtime.id,
|
|
499
|
+
status: "started",
|
|
500
|
+
...(normalizeToolName(block.name)
|
|
501
|
+
? { name: normalizeToolName(block.name) }
|
|
502
|
+
: {}),
|
|
503
|
+
},
|
|
504
|
+
});
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
if (block.kind === "tool_result") {
|
|
508
|
+
lastReportedKind = block.kind;
|
|
509
|
+
lastReasoningPhase = null;
|
|
510
|
+
await queueStreamEvent({
|
|
511
|
+
type: "run.block",
|
|
512
|
+
payload: {
|
|
513
|
+
schema_version: AGENT_STREAM_SCHEMA_VERSION,
|
|
514
|
+
kind: "tool_result",
|
|
515
|
+
runtime: runtime.id,
|
|
516
|
+
status: block.status === "error" ? "error" : "completed",
|
|
517
|
+
...(normalizeToolName(block.name)
|
|
518
|
+
? { name: normalizeToolName(block.name) }
|
|
519
|
+
: {}),
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
if (block.kind === "error") {
|
|
525
|
+
lastReportedKind = block.kind;
|
|
526
|
+
await queueStreamEvent({
|
|
527
|
+
type: "run.block",
|
|
528
|
+
payload: {
|
|
529
|
+
schema_version: AGENT_STREAM_SCHEMA_VERSION,
|
|
530
|
+
kind: "error",
|
|
531
|
+
runtime: runtime.id,
|
|
532
|
+
status: "error",
|
|
533
|
+
},
|
|
534
|
+
});
|
|
535
|
+
}
|
|
377
536
|
},
|
|
378
537
|
message: async (text) => {
|
|
538
|
+
await flushContent();
|
|
379
539
|
finalText = text;
|
|
380
540
|
},
|
|
381
541
|
file: async (file) => {
|
|
@@ -483,6 +643,8 @@ export class RunDispatcher {
|
|
|
483
643
|
finally {
|
|
484
644
|
if (timer !== undefined)
|
|
485
645
|
clearTimeout(timer);
|
|
646
|
+
if (contentFlushTimer !== undefined)
|
|
647
|
+
clearTimeout(contentFlushTimer);
|
|
486
648
|
if (profileApplied)
|
|
487
649
|
cleanupRunRuntimeProfile(runId);
|
|
488
650
|
if (Object.values(progressDisposition).some((count) => count > 0)) {
|
package/dist/runtime-env.js
CHANGED
|
@@ -20,7 +20,8 @@ const AGENT_SERVICE_SUPERVISOR_ENV_KEYS = [
|
|
|
20
20
|
];
|
|
21
21
|
const RUNTIME_USER_PATTERN = /^[a-z_][a-z0-9_-]{0,31}$/;
|
|
22
22
|
const RUNTIME_SUDO_BINARY = "/usr/bin/sudo";
|
|
23
|
-
const MANAGED_RUNTIME_LAUNCHER = "/
|
|
23
|
+
const MANAGED_RUNTIME_LAUNCHER = "/opt/botlearn/bin/botlearn-runtime-launcher";
|
|
24
|
+
const MANAGED_RUNTIME_BINARY = "/opt/deepseek-tui/0.8.39/bin/deepseek";
|
|
24
25
|
/** Remove Course control-plane coordinates before any model/runtime child is created. */
|
|
25
26
|
export function clearAgentServiceControlEnv(env = process.env) {
|
|
26
27
|
for (const key of AGENT_SERVICE_CONTROL_ENV_KEYS)
|
|
@@ -72,6 +73,9 @@ export function runtimeChildLaunch(binary, args, env = process.env) {
|
|
|
72
73
|
if (env.BOTLEARN_RUNTIME_LAUNCHER !== MANAGED_RUNTIME_LAUNCHER) {
|
|
73
74
|
throw new Error("invalid supervisor-provided runtime launcher");
|
|
74
75
|
}
|
|
76
|
+
if (binary !== MANAGED_RUNTIME_BINARY) {
|
|
77
|
+
throw new Error("invalid supervisor-provided runtime binary");
|
|
78
|
+
}
|
|
75
79
|
return {
|
|
76
80
|
binary: RUNTIME_SUDO_BINARY,
|
|
77
81
|
args: [
|
|
@@ -324,22 +324,33 @@ function claudeStatusEvent(obj) {
|
|
|
324
324
|
}
|
|
325
325
|
function normalizeBlock(obj, seq) {
|
|
326
326
|
let kind = "other";
|
|
327
|
+
const contents = Array.isArray(obj?.message?.content) ? obj.message.content : [];
|
|
327
328
|
if (obj?.type === "assistant") {
|
|
328
|
-
const contents = Array.isArray(obj.message?.content) ? obj.message.content : [];
|
|
329
329
|
if (contents.some((c) => c?.type === "tool_use"))
|
|
330
330
|
kind = "tool_use";
|
|
331
331
|
else if (contents.some((c) => c?.type === "text"))
|
|
332
332
|
kind = "assistant_text";
|
|
333
333
|
}
|
|
334
334
|
else if (obj?.type === "user") {
|
|
335
|
-
const contents = Array.isArray(obj.message?.content) ? obj.message.content : [];
|
|
336
335
|
if (contents.some((c) => c?.type === "tool_result"))
|
|
337
336
|
kind = "tool_result";
|
|
338
337
|
}
|
|
339
338
|
else if (obj?.type === "system") {
|
|
340
339
|
kind = "system";
|
|
341
340
|
}
|
|
342
|
-
|
|
341
|
+
const tool = contents.find((c) => c?.type === "tool_use");
|
|
342
|
+
const text = contents
|
|
343
|
+
.filter((c) => c?.type === "text" && typeof c.text === "string")
|
|
344
|
+
.map((c) => c.text)
|
|
345
|
+
.join("");
|
|
346
|
+
return {
|
|
347
|
+
raw: obj,
|
|
348
|
+
kind,
|
|
349
|
+
seq,
|
|
350
|
+
...(kind === "assistant_text" && text ? { text } : {}),
|
|
351
|
+
...(kind === "tool_use" && typeof tool?.name === "string" ? { name: tool.name } : {}),
|
|
352
|
+
...(kind === "tool_result" ? { status: "completed" } : {}),
|
|
353
|
+
};
|
|
343
354
|
}
|
|
344
355
|
export const claudeCodeModule = {
|
|
345
356
|
id: "claude-code",
|
package/dist/runtimes/codex.js
CHANGED
|
@@ -318,7 +318,23 @@ function normalizeBlock(obj, seq) {
|
|
|
318
318
|
kind = type === "item.completed" ? "tool_result" : "tool_use";
|
|
319
319
|
}
|
|
320
320
|
}
|
|
321
|
-
|
|
321
|
+
const toolName = itemType === "mcp_tool_call"
|
|
322
|
+
? obj?.item?.tool ?? obj?.item?.name ?? itemType
|
|
323
|
+
: itemType;
|
|
324
|
+
return {
|
|
325
|
+
raw: obj,
|
|
326
|
+
kind,
|
|
327
|
+
seq,
|
|
328
|
+
...(kind === "assistant_text" && typeof obj?.item?.text === "string"
|
|
329
|
+
? { text: obj.item.text }
|
|
330
|
+
: {}),
|
|
331
|
+
...((kind === "tool_use" || kind === "tool_result") && typeof toolName === "string"
|
|
332
|
+
? { name: toolName }
|
|
333
|
+
: {}),
|
|
334
|
+
...(kind === "tool_result"
|
|
335
|
+
? { status: obj?.item?.status === "failed" ? "error" : "completed" }
|
|
336
|
+
: {}),
|
|
337
|
+
};
|
|
322
338
|
}
|
|
323
339
|
export const codexModule = {
|
|
324
340
|
id: "codex",
|
|
@@ -7,6 +7,18 @@ import { runtimeChildEnv, runtimeChildLaunch } from "../runtime-env.js";
|
|
|
7
7
|
import { readCommandVersion, resolveCommandOnPath } from "./probe.js";
|
|
8
8
|
import { adaptDeepseekProgressStarted, cleanupProgressMcpConfig, createDeepseekProgressState, createProgressMcpConfig, deepseekProgressDispositions, isDeepseekProgressCompletion, progressMcpAutoInjectionSupported, progressSystemContext, } from "./progress.js";
|
|
9
9
|
import { consoleLogger, wrapEngineAdapter, } from "./engine.js";
|
|
10
|
+
class DeepseekHttpError extends Error {
|
|
11
|
+
status;
|
|
12
|
+
constructor(status, detail = "", operation = "") {
|
|
13
|
+
super(`${operation ? `${operation} ` : ""}HTTP ${status}${detail ? `: ${detail.slice(0, 300)}` : ""}`);
|
|
14
|
+
this.status = status;
|
|
15
|
+
this.name = "DeepseekHttpError";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function isMissingThreadHttpError(error) {
|
|
19
|
+
return (error instanceof DeepseekHttpError
|
|
20
|
+
&& (error.status === 404 || error.status === 410));
|
|
21
|
+
}
|
|
10
22
|
const log = consoleLogger;
|
|
11
23
|
const DEEPSEEK_IDLE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
12
24
|
const STARTUP_TIMEOUT_MS = 30_000;
|
|
@@ -150,8 +162,8 @@ export class DeepseekTuiAdapter {
|
|
|
150
162
|
}
|
|
151
163
|
catch (err) {
|
|
152
164
|
const message = err instanceof Error ? err.message : String(err);
|
|
153
|
-
//
|
|
154
|
-
const staleSession = opts.sessionId &&
|
|
165
|
+
// 服务端明确确认线程不存在/已过期时才清空 sessionId,让下一轮从 durable context 重建。
|
|
166
|
+
const staleSession = Boolean(opts.sessionId) && isMissingThreadHttpError(err);
|
|
155
167
|
return {
|
|
156
168
|
text: "",
|
|
157
169
|
newSessionId: staleSession ? "" : (opts.sessionId ?? ""),
|
|
@@ -404,7 +416,7 @@ export class DeepseekTuiAdapter {
|
|
|
404
416
|
async readEvents(url, headers, opts, signal) {
|
|
405
417
|
const res = await this.fetchFn(url, { method: "GET", headers, signal });
|
|
406
418
|
if (!res.ok)
|
|
407
|
-
throw new
|
|
419
|
+
throw new DeepseekHttpError(res.status, "", "events stream failed");
|
|
408
420
|
if (!res.body)
|
|
409
421
|
throw new Error("events stream response missing body");
|
|
410
422
|
const reader = res.body.getReader();
|
|
@@ -538,7 +550,7 @@ export class DeepseekTuiAdapter {
|
|
|
538
550
|
catch {
|
|
539
551
|
// ignore
|
|
540
552
|
}
|
|
541
|
-
throw new
|
|
553
|
+
throw new DeepseekHttpError(res.status, detail);
|
|
542
554
|
}
|
|
543
555
|
return (await res.json());
|
|
544
556
|
}
|
|
@@ -553,16 +565,37 @@ export function __resetDeepseekTuiPoolForTests() {
|
|
|
553
565
|
}
|
|
554
566
|
function normalizeDeepseekEvent(eventName, payload, seq) {
|
|
555
567
|
if (eventName === "message.delta") {
|
|
556
|
-
return {
|
|
568
|
+
return {
|
|
569
|
+
raw: { event: eventName, payload },
|
|
570
|
+
kind: "assistant_text",
|
|
571
|
+
seq,
|
|
572
|
+
text: stringField(payload, "content") ?? "",
|
|
573
|
+
};
|
|
557
574
|
}
|
|
558
575
|
if (eventName === "tool.started" || isToolStarted(eventName, payload)) {
|
|
559
|
-
return {
|
|
576
|
+
return {
|
|
577
|
+
raw: { event: eventName, payload },
|
|
578
|
+
kind: "tool_use",
|
|
579
|
+
seq,
|
|
580
|
+
...(deepseekToolName(payload) ? { name: deepseekToolName(payload) } : {}),
|
|
581
|
+
};
|
|
560
582
|
}
|
|
561
583
|
if (eventName === "tool.completed" || isToolCompleted(eventName, payload)) {
|
|
562
|
-
return {
|
|
584
|
+
return {
|
|
585
|
+
raw: { event: eventName, payload },
|
|
586
|
+
kind: "tool_result",
|
|
587
|
+
seq,
|
|
588
|
+
...(deepseekToolName(payload) ? { name: deepseekToolName(payload) } : {}),
|
|
589
|
+
status: deepseekToolFailed(payload) ? "error" : "completed",
|
|
590
|
+
};
|
|
563
591
|
}
|
|
564
592
|
if (eventName === "item.delta" && isAgentMessageDelta(payload)) {
|
|
565
|
-
return {
|
|
593
|
+
return {
|
|
594
|
+
raw: { event: eventName, payload },
|
|
595
|
+
kind: "assistant_text",
|
|
596
|
+
seq,
|
|
597
|
+
text: extractDeepseekDelta(payload),
|
|
598
|
+
};
|
|
566
599
|
}
|
|
567
600
|
if (eventName === "item.completed" && isAgentReasoningItem(payload)) {
|
|
568
601
|
return { raw: { event: eventName, payload }, kind: "thinking", seq };
|
|
@@ -674,6 +707,19 @@ function inferDeepseekToolName(item) {
|
|
|
674
707
|
}
|
|
675
708
|
return undefined;
|
|
676
709
|
}
|
|
710
|
+
function deepseekToolName(payload) {
|
|
711
|
+
return (stringField(payload, "name")
|
|
712
|
+
?? stringField(payload?.tool, "name")
|
|
713
|
+
?? stringField(payload?.payload?.tool, "name")
|
|
714
|
+
?? inferDeepseekToolName(payload?.item ?? payload?.payload?.item));
|
|
715
|
+
}
|
|
716
|
+
function deepseekToolFailed(payload) {
|
|
717
|
+
const status = (stringField(payload, "status")
|
|
718
|
+
?? stringField(payload?.tool, "status")
|
|
719
|
+
?? stringField(payload?.payload?.tool, "status")
|
|
720
|
+
?? "").toLowerCase();
|
|
721
|
+
return status.includes("fail") || status.includes("error");
|
|
722
|
+
}
|
|
677
723
|
function emptyCompletionError(stderrTail) {
|
|
678
724
|
const tail = stderrTail.trim();
|
|
679
725
|
if (!tail) {
|
|
@@ -9,6 +9,11 @@ export interface ContentStreamBlock {
|
|
|
9
9
|
raw: unknown;
|
|
10
10
|
kind: "assistant_text" | "tool_use" | "tool_result" | "system" | "thinking" | "other";
|
|
11
11
|
seq: number;
|
|
12
|
+
/** Assistant-visible text only. Provider raw payloads must never be used as browser text. */
|
|
13
|
+
text?: string;
|
|
14
|
+
/** Safe provider-normalized tool identifier; arguments and results remain private. */
|
|
15
|
+
name?: string;
|
|
16
|
+
status?: "completed" | "error";
|
|
12
17
|
}
|
|
13
18
|
/** provider 已严格校验的进度块;禁止携带原始 tool envelope。 */
|
|
14
19
|
export interface ProgressStreamBlock {
|
package/dist/runtimes/engine.js
CHANGED
|
@@ -88,7 +88,12 @@ export function wrapEngineAdapter(id, engine, opts) {
|
|
|
88
88
|
id,
|
|
89
89
|
async run(run, sink, signal) {
|
|
90
90
|
const payload = run.payload;
|
|
91
|
-
|
|
91
|
+
// DeepSeek persists and replays the native thread history. The durable Course Service
|
|
92
|
+
// conversation is recovery/bootstrap data, not a second history to inject on every turn.
|
|
93
|
+
const resumesDeepseekThread = id === "deepseek-tui" && Boolean(run.nativeSessionId?.trim());
|
|
94
|
+
const text = resumesDeepseekThread
|
|
95
|
+
? (payload.input.text ?? "")
|
|
96
|
+
: renderConversationInput(payload);
|
|
92
97
|
if (!text.trim()) {
|
|
93
98
|
throw new RuntimeExecutionError("empty task brief");
|
|
94
99
|
}
|
|
@@ -131,10 +136,22 @@ export function wrapEngineAdapter(id, engine, opts) {
|
|
|
131
136
|
return;
|
|
132
137
|
}
|
|
133
138
|
const kind = BLOCK_KIND_MAP[block.kind] ?? "status";
|
|
134
|
-
queueBlock({
|
|
139
|
+
queueBlock({
|
|
140
|
+
kind,
|
|
141
|
+
raw: block.raw,
|
|
142
|
+
...(block.text !== undefined ? { text: block.text } : {}),
|
|
143
|
+
...(block.name !== undefined ? { name: block.name } : {}),
|
|
144
|
+
...(block.status !== undefined ? { status: block.status } : {}),
|
|
145
|
+
});
|
|
135
146
|
},
|
|
136
147
|
onStatus: (event) => {
|
|
137
148
|
consoleLogger.debug(`${id} status`, { kind: event.kind, phase: event.phase });
|
|
149
|
+
if (event.kind === "thinking") {
|
|
150
|
+
queueBlock({
|
|
151
|
+
kind: "thinking",
|
|
152
|
+
phase: event.phase === "stopped" ? "completed" : "in_progress",
|
|
153
|
+
});
|
|
154
|
+
}
|
|
138
155
|
},
|
|
139
156
|
});
|
|
140
157
|
// Preserve provider event order through durable run.block before the final run.message.
|
package/dist/runtimes/gemini.js
CHANGED
|
@@ -238,7 +238,20 @@ function normalizeBlock(obj, seq) {
|
|
|
238
238
|
else if (type === "init" || type === "result") {
|
|
239
239
|
kind = "system";
|
|
240
240
|
}
|
|
241
|
-
return {
|
|
241
|
+
return {
|
|
242
|
+
raw: obj,
|
|
243
|
+
kind,
|
|
244
|
+
seq,
|
|
245
|
+
...(kind === "assistant_text" && typeof obj.content === "string"
|
|
246
|
+
? { text: obj.content }
|
|
247
|
+
: {}),
|
|
248
|
+
...(kind === "tool_use" && typeof obj.tool_name === "string"
|
|
249
|
+
? { name: obj.tool_name }
|
|
250
|
+
: {}),
|
|
251
|
+
...(kind === "tool_result"
|
|
252
|
+
? { status: obj.status === "error" ? "error" : "completed" }
|
|
253
|
+
: {}),
|
|
254
|
+
};
|
|
242
255
|
}
|
|
243
256
|
export const geminiModule = {
|
|
244
257
|
id: "gemini",
|
|
@@ -123,7 +123,7 @@ export class HermesAgentAdapter extends AcpRuntimeAdapter {
|
|
|
123
123
|
blockKind = "assistant_text";
|
|
124
124
|
}
|
|
125
125
|
else if (kind === "tool_call" || kind === "tool_call_update") {
|
|
126
|
-
blockKind = "tool_use";
|
|
126
|
+
blockKind = kind === "tool_call_update" ? "tool_result" : "tool_use";
|
|
127
127
|
}
|
|
128
128
|
else if (kind === "user_message_chunk") {
|
|
129
129
|
blockKind = "other";
|
|
@@ -132,7 +132,24 @@ export class HermesAgentAdapter extends AcpRuntimeAdapter {
|
|
|
132
132
|
const status = hermesStatusEvent(kind, update, assistantTextSeen);
|
|
133
133
|
if (status)
|
|
134
134
|
ctx.emitStatus(status);
|
|
135
|
-
|
|
135
|
+
const tool = update.toolCall;
|
|
136
|
+
ctx.emitBlock({
|
|
137
|
+
raw: params,
|
|
138
|
+
kind: blockKind,
|
|
139
|
+
seq: ctx.seq,
|
|
140
|
+
...(blockKind === "assistant_text" && kind === "agent_message_chunk"
|
|
141
|
+
? {
|
|
142
|
+
text: update.content?.text ?? "",
|
|
143
|
+
}
|
|
144
|
+
: {}),
|
|
145
|
+
...((blockKind === "tool_use" || blockKind === "tool_result")
|
|
146
|
+
&& typeof tool?.name === "string"
|
|
147
|
+
? { name: tool.name }
|
|
148
|
+
: {}),
|
|
149
|
+
...(blockKind === "tool_result"
|
|
150
|
+
? { status: tool?.status === "failed" ? "error" : "completed" }
|
|
151
|
+
: {}),
|
|
152
|
+
});
|
|
136
153
|
}
|
|
137
154
|
/**
|
|
138
155
|
* owner 信任:选第一个 `kind` 以 `allow_` 开头的选项,没有再退回第一个
|
package/dist/runtimes/kimi.js
CHANGED
|
@@ -323,7 +323,15 @@ function normalizeBlock(obj, seq) {
|
|
|
323
323
|
else if (obj.category || obj.severity) {
|
|
324
324
|
kind = "system";
|
|
325
325
|
}
|
|
326
|
-
|
|
326
|
+
const text = kind === "assistant_text" ? extractText(obj.content) : "";
|
|
327
|
+
return {
|
|
328
|
+
raw: obj,
|
|
329
|
+
kind,
|
|
330
|
+
seq,
|
|
331
|
+
...(text ? { text } : {}),
|
|
332
|
+
...(kind === "tool_use" ? { name: firstToolName(obj.tool_calls) } : {}),
|
|
333
|
+
...(kind === "tool_result" ? { status: "completed" } : {}),
|
|
334
|
+
};
|
|
327
335
|
}
|
|
328
336
|
export const kimiCliModule = {
|
|
329
337
|
id: "kimi-cli",
|
|
@@ -177,12 +177,30 @@ export class OpenclawAcpAdapter {
|
|
|
177
177
|
if (!text)
|
|
178
178
|
return;
|
|
179
179
|
seq += 1;
|
|
180
|
-
emitBlock({
|
|
180
|
+
emitBlock({
|
|
181
|
+
raw: sanitizeAssistantChunk(note, text),
|
|
182
|
+
kind: "assistant_text",
|
|
183
|
+
seq,
|
|
184
|
+
text,
|
|
185
|
+
});
|
|
181
186
|
return;
|
|
182
187
|
}
|
|
183
188
|
seq += 1;
|
|
184
189
|
const kind = classifyAcpUpdate(note);
|
|
185
|
-
|
|
190
|
+
const toolCall = update?.toolCall;
|
|
191
|
+
const toolName = toolCall && typeof toolCall.name === "string" ? toolCall.name : undefined;
|
|
192
|
+
const toolStatus = toolCall && typeof toolCall.status === "string" ? toolCall.status.toLowerCase() : "";
|
|
193
|
+
emitBlock({
|
|
194
|
+
raw: note,
|
|
195
|
+
kind,
|
|
196
|
+
seq,
|
|
197
|
+
...((kind === "tool_use" || kind === "tool_result") && toolName
|
|
198
|
+
? { name: toolName }
|
|
199
|
+
: {}),
|
|
200
|
+
...(kind === "tool_result"
|
|
201
|
+
? { status: /fail|error/.test(toolStatus) ? "error" : "completed" }
|
|
202
|
+
: {}),
|
|
203
|
+
});
|
|
186
204
|
};
|
|
187
205
|
let abortListener;
|
|
188
206
|
try {
|
|
@@ -293,6 +311,7 @@ export class OpenclawAcpAdapter {
|
|
|
293
311
|
},
|
|
294
312
|
kind: "assistant_text",
|
|
295
313
|
seq,
|
|
314
|
+
text: textForBlock,
|
|
296
315
|
});
|
|
297
316
|
}
|
|
298
317
|
}
|
|
@@ -23,6 +23,11 @@ export interface ProgressMcpConfig {
|
|
|
23
23
|
export interface ProgressMcpConfigOptions {
|
|
24
24
|
/** undefined auto-discovers explicit/default config; null creates a progress-only config. */
|
|
25
25
|
baseConfigPath?: string | null;
|
|
26
|
+
/**
|
|
27
|
+
* undefined auto-detects the managed runtime boundary, null forces a private BYOA
|
|
28
|
+
* config, and a path writes a sanitized runtime-readable config below that root.
|
|
29
|
+
*/
|
|
30
|
+
managedRoot?: string | null;
|
|
26
31
|
platform?: NodeJS.Platform;
|
|
27
32
|
}
|
|
28
33
|
export declare class ProgressMcpConfigError extends Error {
|
|
@@ -45,6 +50,13 @@ export declare function isDeepseekProgressCompletion(payload: unknown, state: De
|
|
|
45
50
|
*/
|
|
46
51
|
export declare function progressMcpAutoInjectionSupported(platform?: NodeJS.Platform): boolean;
|
|
47
52
|
export declare function resolveExistingDeepseekMcpConfig(env?: NodeJS.ProcessEnv, home?: string): string | null;
|
|
48
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* Create an ephemeral DeepSeek MCP config.
|
|
55
|
+
*
|
|
56
|
+
* BYOA keeps the user's existing MCP settings in a private 0700/0600 directory. Managed
|
|
57
|
+
* Agent Service sessions instead write a progress-only 0750/0640 handoff below the
|
|
58
|
+
* supervisor-owned profile root so the separate runtime UID can read it without exposing
|
|
59
|
+
* control-plane configuration.
|
|
60
|
+
*/
|
|
49
61
|
export declare function createProgressMcpConfig(options?: ProgressMcpConfigOptions): ProgressMcpConfig;
|
|
50
62
|
export declare function cleanupProgressMcpConfig(config: ProgressMcpConfig | undefined): void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
1
|
+
import { chmodSync, existsSync, lstatSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
2
2
|
import { homedir, tmpdir } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
@@ -10,6 +10,7 @@ export const DEEPSEEK_PROGRESS_TOOL_ALIASES = new Set([
|
|
|
10
10
|
]);
|
|
11
11
|
export const DEEPSEEK_PROGRESS_SYSTEM_INSTRUCTION = [
|
|
12
12
|
"BotLearn execution progress reporting:",
|
|
13
|
+
"- DeepSeek may defer this MCP tool. Before the first meaningful progress phase, if report_progress is not exposed, call tool_search_tool_regex once with query report_progress, then call the discovered progress tool.",
|
|
13
14
|
"- Use report_progress only when a meaningful user-visible execution phase starts or completes.",
|
|
14
15
|
"- Use status in_progress at phase start and completed only when that execution phase actually ends.",
|
|
15
16
|
"- Short tasks need no progress report; do not report every command, file read, or retry.",
|
|
@@ -120,22 +121,37 @@ export function resolveExistingDeepseekMcpConfig(env = process.env, home = homed
|
|
|
120
121
|
const defaultPath = path.join(home, ".deepseek", "mcp.json");
|
|
121
122
|
return existsSync(defaultPath) ? defaultPath : null;
|
|
122
123
|
}
|
|
123
|
-
/**
|
|
124
|
+
/**
|
|
125
|
+
* Create an ephemeral DeepSeek MCP config.
|
|
126
|
+
*
|
|
127
|
+
* BYOA keeps the user's existing MCP settings in a private 0700/0600 directory. Managed
|
|
128
|
+
* Agent Service sessions instead write a progress-only 0750/0640 handoff below the
|
|
129
|
+
* supervisor-owned profile root so the separate runtime UID can read it without exposing
|
|
130
|
+
* control-plane configuration.
|
|
131
|
+
*/
|
|
124
132
|
export function createProgressMcpConfig(options = {}) {
|
|
125
133
|
const platform = options.platform ?? process.platform;
|
|
126
134
|
if (!progressMcpAutoInjectionSupported(platform)) {
|
|
127
135
|
throw new ProgressMcpConfigError("report_progress MCP auto-injection is unavailable on Windows because clean child env isolation cannot be guaranteed");
|
|
128
136
|
}
|
|
137
|
+
const managedRoot = resolveManagedProgressRoot(options.managedRoot);
|
|
138
|
+
if (managedRoot
|
|
139
|
+
&& options.baseConfigPath !== undefined
|
|
140
|
+
&& options.baseConfigPath !== null) {
|
|
141
|
+
throw new ProgressMcpConfigError("managed progress MCP config cannot preserve an existing DeepSeek MCP config");
|
|
142
|
+
}
|
|
129
143
|
const serverPath = fileURLToPath(new URL("../mcp/report-progress-server.js", import.meta.url));
|
|
130
|
-
const baseConfigPath =
|
|
131
|
-
?
|
|
132
|
-
: options.baseConfigPath
|
|
144
|
+
const baseConfigPath = managedRoot
|
|
145
|
+
? null
|
|
146
|
+
: options.baseConfigPath === undefined
|
|
147
|
+
? resolveExistingDeepseekMcpConfig()
|
|
148
|
+
: options.baseConfigPath;
|
|
133
149
|
const baseConfig = loadBaseMcpConfig(baseConfigPath);
|
|
134
150
|
const baseServers = mergeMcpServerFields(baseConfig);
|
|
135
151
|
if (Object.hasOwn(baseServers, "botlearn")) {
|
|
136
152
|
throw new ProgressMcpConfigError("DeepSeek MCP server key 'botlearn' is reserved for BotLearn progress reporting");
|
|
137
153
|
}
|
|
138
|
-
const dir = mkdtempSync(path.join(tmpdir(), "botlearn-progress-mcp-"));
|
|
154
|
+
const dir = mkdtempSync(path.join(managedRoot ?? tmpdir(), "botlearn-progress-mcp-"));
|
|
139
155
|
const configPath = path.join(dir, "mcp.json");
|
|
140
156
|
const stagingPath = path.join(dir, ".mcp.json.tmp");
|
|
141
157
|
const minimalPath = "/usr/bin:/bin";
|
|
@@ -157,7 +173,11 @@ export function createProgressMcpConfig(options = {}) {
|
|
|
157
173
|
},
|
|
158
174
|
};
|
|
159
175
|
try {
|
|
176
|
+
if (managedRoot)
|
|
177
|
+
chmodSync(dir, 0o750);
|
|
160
178
|
writeFileSync(stagingPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
179
|
+
if (managedRoot)
|
|
180
|
+
chmodSync(stagingPath, 0o640);
|
|
161
181
|
renameSync(stagingPath, configPath);
|
|
162
182
|
return { dir, path: configPath };
|
|
163
183
|
}
|
|
@@ -166,6 +186,41 @@ export function createProgressMcpConfig(options = {}) {
|
|
|
166
186
|
throw error;
|
|
167
187
|
}
|
|
168
188
|
}
|
|
189
|
+
function resolveManagedProgressRoot(explicit) {
|
|
190
|
+
if (explicit === null)
|
|
191
|
+
return null;
|
|
192
|
+
let root = explicit?.trim();
|
|
193
|
+
if (root === undefined) {
|
|
194
|
+
const managedRuntime = process.env.BOTLEARN_RUNTIME_USER?.trim();
|
|
195
|
+
if (!managedRuntime)
|
|
196
|
+
return null;
|
|
197
|
+
root = process.env.BOTLEARN_AGENT_SERVICE_PROFILE_ROOT?.trim();
|
|
198
|
+
if (!root) {
|
|
199
|
+
throw new ProgressMcpConfigError("managed runtime is missing BOTLEARN_AGENT_SERVICE_PROFILE_ROOT");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (!root || !path.isAbsolute(root)) {
|
|
203
|
+
throw new ProgressMcpConfigError("managed progress MCP root must be an absolute path");
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const stat = lstatSync(root);
|
|
207
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
208
|
+
throw new Error("root is not a regular directory");
|
|
209
|
+
}
|
|
210
|
+
const currentUid = process.getuid?.();
|
|
211
|
+
if (currentUid !== undefined && stat.uid !== currentUid) {
|
|
212
|
+
throw new Error("root is not owned by the control process uid");
|
|
213
|
+
}
|
|
214
|
+
if ((stat.mode & 0o022) !== 0) {
|
|
215
|
+
throw new Error("root must not be group or world writable");
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
220
|
+
throw new ProgressMcpConfigError(`managed progress MCP root is unavailable: ${message}`);
|
|
221
|
+
}
|
|
222
|
+
return root;
|
|
223
|
+
}
|
|
169
224
|
function loadBaseMcpConfig(configPath) {
|
|
170
225
|
if (!configPath)
|
|
171
226
|
return {};
|
|
@@ -10,7 +10,19 @@ const CONTROL_HOME = "/home/botlearn-control/.botlearn-course/daemon";
|
|
|
10
10
|
const WORKSPACE = "/workspace";
|
|
11
11
|
const RUNTIME_PROFILE_ROOT = "/run/botlearn-runtime-profiles";
|
|
12
12
|
const SUPERVISOR_LOCK_ROOT = "/run/botlearn-sandbox-supervisors";
|
|
13
|
-
const
|
|
13
|
+
const NODE_BINARY = "/opt/node/22.23.1/bin/node";
|
|
14
|
+
const DAEMON_ENTRY = "/opt/botlearn/course-daemon/dist/cli.js";
|
|
15
|
+
const RUNTIME_LAUNCHER = "/opt/botlearn/bin/botlearn-runtime-launcher";
|
|
16
|
+
const DEEPSEEK_BINARY = "/opt/deepseek-tui/0.8.39/bin/deepseek";
|
|
17
|
+
const MANAGED_PATH = [
|
|
18
|
+
"/opt/deepseek-tui/0.8.39/bin",
|
|
19
|
+
"/opt/node/22.23.1/bin",
|
|
20
|
+
"/usr/bin",
|
|
21
|
+
"/bin",
|
|
22
|
+
// E2B materializes this directory as runtime-writable. Keep it last and never
|
|
23
|
+
// use it for a managed control-plane executable.
|
|
24
|
+
"/usr/local/bin",
|
|
25
|
+
].join(":");
|
|
14
26
|
function numericId(flag, user) {
|
|
15
27
|
const output = execFileSync("/usr/bin/id", [flag, user], {
|
|
16
28
|
encoding: "utf8",
|
|
@@ -117,19 +129,25 @@ export async function runSandboxSupervisor(argv) {
|
|
|
117
129
|
if (releaseLock === null)
|
|
118
130
|
return 0;
|
|
119
131
|
prepareDirectories(controlUid, controlGid, runtimeUid, runtimeSessionId, sessionGeneration);
|
|
120
|
-
child = spawn(
|
|
132
|
+
child = spawn(NODE_BINARY, [
|
|
133
|
+
DAEMON_ENTRY,
|
|
134
|
+
"agent-service",
|
|
135
|
+
"session",
|
|
136
|
+
"--bootstrap-stdin",
|
|
137
|
+
], {
|
|
121
138
|
uid: controlUid,
|
|
122
139
|
gid: controlGid,
|
|
123
140
|
env: {
|
|
124
141
|
HOME: "/home/botlearn-control",
|
|
125
|
-
PATH:
|
|
142
|
+
PATH: MANAGED_PATH,
|
|
126
143
|
BOTLEARN_DAEMON_HOME: CONTROL_HOME,
|
|
127
144
|
BOTLEARN_RUNTIME_UID: String(runtimeUid),
|
|
128
145
|
BOTLEARN_RUNTIME_GID: String(controlGid),
|
|
129
146
|
BOTLEARN_RUNTIME_USER: RUNTIME_USER,
|
|
130
147
|
BOTLEARN_RUNTIME_GROUP: CONTROL_USER,
|
|
131
148
|
BOTLEARN_RUNTIME_HOME: "/home/user",
|
|
132
|
-
BOTLEARN_RUNTIME_LAUNCHER:
|
|
149
|
+
BOTLEARN_RUNTIME_LAUNCHER: RUNTIME_LAUNCHER,
|
|
150
|
+
BOTLEARN_DEEPSEEK_TUI_BIN: DEEPSEEK_BINARY,
|
|
133
151
|
BOTLEARN_AGENT_SERVICE_WORKSPACE_ROOT: WORKSPACE,
|
|
134
152
|
BOTLEARN_AGENT_SERVICE_PROFILE_ROOT: RUNTIME_PROFILE_ROOT,
|
|
135
153
|
},
|
package/dist/types.d.ts
CHANGED
|
@@ -114,6 +114,11 @@ export interface AppliedRunRuntimeProfile {
|
|
|
114
114
|
export interface RuntimeContentBlock {
|
|
115
115
|
kind: "text_delta" | "text" | "thinking" | "tool_call" | "tool_result" | "status" | "error";
|
|
116
116
|
text?: string;
|
|
117
|
+
/** Safe provider-normalized tool identifier. Raw arguments/results stay in the transcript only. */
|
|
118
|
+
name?: string;
|
|
119
|
+
/** Public lifecycle metadata; never carries provider reasoning or tool output. */
|
|
120
|
+
phase?: "in_progress" | "completed";
|
|
121
|
+
status?: "completed" | "error";
|
|
117
122
|
raw?: unknown;
|
|
118
123
|
}
|
|
119
124
|
/** 已由 provider adapter 严格归一化、无 provider raw envelope 的进度遥测。 */
|
package/package.json
CHANGED