@botlearn-course/daemon 0.0.6 → 0.0.8
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 +3 -0
- package/dist/agent-service-client.js +8 -8
- package/dist/agent-service-sandbox.d.ts +108 -0
- package/dist/agent-service-sandbox.js +1400 -0
- package/dist/agent-service-ws-protocol.d.ts +15 -7
- package/dist/agent-service-ws-protocol.js +86 -24
- package/dist/cli.js +26 -53
- package/dist/course-client.js +9 -9
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/run-dispatcher.d.ts +3 -0
- package/dist/run-dispatcher.js +194 -13
- package/dist/runtime-env.d.ts +6 -0
- package/dist/runtime-env.js +35 -2
- package/dist/runtime-profile.js +12 -1
- package/dist/runtimes/claude-code.js +14 -3
- package/dist/runtimes/codex.js +17 -1
- package/dist/runtimes/deepseek-tui.d.ts +3 -2
- package/dist/runtimes/deepseek-tui.js +87 -15
- package/dist/runtimes/engine.d.ts +5 -0
- package/dist/runtimes/engine.js +22 -2
- package/dist/runtimes/fake.js +102 -6
- 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.d.ts +1 -1
- package/dist/sandbox-supervisor.js +23 -23
- package/dist/types.d.ts +5 -0
- package/dist/workspace.d.ts +27 -4
- package/dist/workspace.js +96 -23
- package/package.json +1 -1
- package/dist/agent-service-session.d.ts +0 -67
- package/dist/agent-service-session.js +0 -796
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。
|
|
@@ -54,7 +72,10 @@ export class RunDispatcher {
|
|
|
54
72
|
}
|
|
55
73
|
/** 排队执行一个 run。返回的 Promise 不 reject(失败已归一化回报为 run.failed)。 */
|
|
56
74
|
dispatch(payload) {
|
|
57
|
-
|
|
75
|
+
// 持久 sandbox 内跨 session 全局串行:整个 sandbox 同时只有一个 active turn(ADR-015 §7/§9)。
|
|
76
|
+
const key = this.persistentSession
|
|
77
|
+
? "persistent-sandbox"
|
|
78
|
+
: payload.agent_instance_id ?? payload.agent_run_id;
|
|
58
79
|
this.scheduledRunIds.add(payload.agent_run_id);
|
|
59
80
|
return this.queue
|
|
60
81
|
.enqueue(key, () => this.execute(payload))
|
|
@@ -85,6 +106,17 @@ export class RunDispatcher {
|
|
|
85
106
|
get activeCount() {
|
|
86
107
|
return this.inflight.size;
|
|
87
108
|
}
|
|
109
|
+
/** Wait until the selected runs have left both the active and queued sets. */
|
|
110
|
+
async waitForRuns(agentRunIds, timeoutMs) {
|
|
111
|
+
const selected = new Set(agentRunIds);
|
|
112
|
+
const deadline = this.now() + timeoutMs;
|
|
113
|
+
while ([...selected].some((runId) => this.inflight.has(runId) || this.scheduledRunIds.has(runId))) {
|
|
114
|
+
if (this.now() >= deadline)
|
|
115
|
+
return false;
|
|
116
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
117
|
+
}
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
88
120
|
/** 等待所有 run(含排队中的)结束;超时返回 false。 */
|
|
89
121
|
async drain(timeoutMs) {
|
|
90
122
|
const deadline = this.now() + timeoutMs;
|
|
@@ -110,6 +142,7 @@ export class RunDispatcher {
|
|
|
110
142
|
let terminalSent = false;
|
|
111
143
|
let timedOut = false;
|
|
112
144
|
let timer;
|
|
145
|
+
let contentFlushTimer;
|
|
113
146
|
let profileApplied = false;
|
|
114
147
|
let toolCalls = 0;
|
|
115
148
|
let toolLimitExceeded = false;
|
|
@@ -265,8 +298,80 @@ export class RunDispatcher {
|
|
|
265
298
|
controller.abort();
|
|
266
299
|
}, timeoutSeconds * 1000);
|
|
267
300
|
let finalText = "";
|
|
268
|
-
// 上一次真正上了 wire 的块 kind:
|
|
301
|
+
// 上一次真正上了 wire 的块 kind:status 只在 kind 切换时上报一次,避免刷屏。
|
|
269
302
|
let lastReportedKind = null;
|
|
303
|
+
let lastReasoningPhase = null;
|
|
304
|
+
let pendingContent = "";
|
|
305
|
+
let streamEventFailure;
|
|
306
|
+
let streamEventChain = Promise.resolve();
|
|
307
|
+
const queueStreamEvent = (event) => {
|
|
308
|
+
const operation = streamEventChain.then(async () => {
|
|
309
|
+
if (streamEventFailure)
|
|
310
|
+
throw streamEventFailure;
|
|
311
|
+
await send(event);
|
|
312
|
+
});
|
|
313
|
+
streamEventChain = operation.catch((error) => {
|
|
314
|
+
streamEventFailure ??= error;
|
|
315
|
+
});
|
|
316
|
+
return operation;
|
|
317
|
+
};
|
|
318
|
+
const awaitStreamEvents = async () => {
|
|
319
|
+
await streamEventChain;
|
|
320
|
+
if (streamEventFailure)
|
|
321
|
+
throw streamEventFailure;
|
|
322
|
+
};
|
|
323
|
+
const flushContent = async () => {
|
|
324
|
+
if (contentFlushTimer !== undefined) {
|
|
325
|
+
clearTimeout(contentFlushTimer);
|
|
326
|
+
contentFlushTimer = undefined;
|
|
327
|
+
}
|
|
328
|
+
if (!pendingContent) {
|
|
329
|
+
await awaitStreamEvents();
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const content = pendingContent;
|
|
333
|
+
pendingContent = "";
|
|
334
|
+
for (const text of chunkUnicodeText(redactSecretString(content), BLOCK_TEXT_MAX_CHARS)) {
|
|
335
|
+
if (!text)
|
|
336
|
+
continue;
|
|
337
|
+
await queueStreamEvent({
|
|
338
|
+
type: "run.block",
|
|
339
|
+
text,
|
|
340
|
+
payload: {
|
|
341
|
+
schema_version: AGENT_STREAM_SCHEMA_VERSION,
|
|
342
|
+
kind: "text_delta",
|
|
343
|
+
runtime: runtime.id,
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
await awaitStreamEvents();
|
|
348
|
+
};
|
|
349
|
+
const scheduleContentFlush = () => {
|
|
350
|
+
if (contentFlushTimer !== undefined)
|
|
351
|
+
return;
|
|
352
|
+
contentFlushTimer = setTimeout(() => {
|
|
353
|
+
contentFlushTimer = undefined;
|
|
354
|
+
void flushContent().catch((error) => {
|
|
355
|
+
streamEventFailure ??= error;
|
|
356
|
+
});
|
|
357
|
+
}, CONTENT_FLUSH_INTERVAL_MS);
|
|
358
|
+
};
|
|
359
|
+
const sendReasoningPhase = async (phase) => {
|
|
360
|
+
if (phase === "completed" && lastReasoningPhase !== "in_progress")
|
|
361
|
+
return;
|
|
362
|
+
if (phase === lastReasoningPhase)
|
|
363
|
+
return;
|
|
364
|
+
lastReasoningPhase = phase;
|
|
365
|
+
await queueStreamEvent({
|
|
366
|
+
type: "run.block",
|
|
367
|
+
payload: {
|
|
368
|
+
schema_version: AGENT_STREAM_SCHEMA_VERSION,
|
|
369
|
+
kind: "reasoning",
|
|
370
|
+
runtime: runtime.id,
|
|
371
|
+
phase,
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
};
|
|
270
375
|
const sink = {
|
|
271
376
|
progressDispositions: async (dispositions) => {
|
|
272
377
|
progressDisposition.invalid += dispositions.invalid;
|
|
@@ -287,6 +392,7 @@ export class RunDispatcher {
|
|
|
287
392
|
},
|
|
288
393
|
block: async (block) => {
|
|
289
394
|
if (block.kind === "progress") {
|
|
395
|
+
await flushContent();
|
|
290
396
|
const normalized = tryNormalizeProgressReport({
|
|
291
397
|
summary: block.summary,
|
|
292
398
|
status: block.status,
|
|
@@ -363,19 +469,87 @@ export class RunDispatcher {
|
|
|
363
469
|
}
|
|
364
470
|
if (serverTerminal)
|
|
365
471
|
return;
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
472
|
+
if (block.kind === "text_delta") {
|
|
473
|
+
if (!block.text)
|
|
474
|
+
return;
|
|
475
|
+
pendingContent += block.text;
|
|
476
|
+
if (pendingContent.length >= CONTENT_FLUSH_MAX_CHARS) {
|
|
477
|
+
await flushContent();
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
scheduleContentFlush();
|
|
481
|
+
}
|
|
370
482
|
return;
|
|
371
|
-
|
|
372
|
-
await
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
}
|
|
483
|
+
}
|
|
484
|
+
await flushContent();
|
|
485
|
+
if (block.kind === "thinking") {
|
|
486
|
+
await sendReasoningPhase(block.phase ?? "in_progress");
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (block.kind === "status") {
|
|
490
|
+
if (lastReportedKind === "status")
|
|
491
|
+
return;
|
|
492
|
+
lastReportedKind = "status";
|
|
493
|
+
await queueStreamEvent({
|
|
494
|
+
type: "run.block",
|
|
495
|
+
payload: {
|
|
496
|
+
schema_version: AGENT_STREAM_SCHEMA_VERSION,
|
|
497
|
+
kind: "status",
|
|
498
|
+
runtime: runtime.id,
|
|
499
|
+
phase: "executing",
|
|
500
|
+
},
|
|
501
|
+
});
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (block.kind === "tool_call") {
|
|
505
|
+
lastReportedKind = block.kind;
|
|
506
|
+
lastReasoningPhase = null;
|
|
507
|
+
await queueStreamEvent({
|
|
508
|
+
type: "run.block",
|
|
509
|
+
payload: {
|
|
510
|
+
schema_version: AGENT_STREAM_SCHEMA_VERSION,
|
|
511
|
+
kind: "tool_call",
|
|
512
|
+
runtime: runtime.id,
|
|
513
|
+
status: "started",
|
|
514
|
+
...(normalizeToolName(block.name)
|
|
515
|
+
? { name: normalizeToolName(block.name) }
|
|
516
|
+
: {}),
|
|
517
|
+
},
|
|
518
|
+
});
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
if (block.kind === "tool_result") {
|
|
522
|
+
lastReportedKind = block.kind;
|
|
523
|
+
lastReasoningPhase = null;
|
|
524
|
+
await queueStreamEvent({
|
|
525
|
+
type: "run.block",
|
|
526
|
+
payload: {
|
|
527
|
+
schema_version: AGENT_STREAM_SCHEMA_VERSION,
|
|
528
|
+
kind: "tool_result",
|
|
529
|
+
runtime: runtime.id,
|
|
530
|
+
status: block.status === "error" ? "error" : "completed",
|
|
531
|
+
...(normalizeToolName(block.name)
|
|
532
|
+
? { name: normalizeToolName(block.name) }
|
|
533
|
+
: {}),
|
|
534
|
+
},
|
|
535
|
+
});
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (block.kind === "error") {
|
|
539
|
+
lastReportedKind = block.kind;
|
|
540
|
+
await queueStreamEvent({
|
|
541
|
+
type: "run.block",
|
|
542
|
+
payload: {
|
|
543
|
+
schema_version: AGENT_STREAM_SCHEMA_VERSION,
|
|
544
|
+
kind: "error",
|
|
545
|
+
runtime: runtime.id,
|
|
546
|
+
status: "error",
|
|
547
|
+
},
|
|
548
|
+
});
|
|
549
|
+
}
|
|
377
550
|
},
|
|
378
551
|
message: async (text) => {
|
|
552
|
+
await flushContent();
|
|
379
553
|
finalText = text;
|
|
380
554
|
},
|
|
381
555
|
file: async (file) => {
|
|
@@ -483,6 +657,8 @@ export class RunDispatcher {
|
|
|
483
657
|
finally {
|
|
484
658
|
if (timer !== undefined)
|
|
485
659
|
clearTimeout(timer);
|
|
660
|
+
if (contentFlushTimer !== undefined)
|
|
661
|
+
clearTimeout(contentFlushTimer);
|
|
486
662
|
if (profileApplied)
|
|
487
663
|
cleanupRunRuntimeProfile(runId);
|
|
488
664
|
if (Object.values(progressDisposition).some((count) => count > 0)) {
|
|
@@ -492,7 +668,12 @@ export class RunDispatcher {
|
|
|
492
668
|
...progressDisposition,
|
|
493
669
|
});
|
|
494
670
|
}
|
|
495
|
-
|
|
671
|
+
try {
|
|
672
|
+
this.persistentSession?.finishTurn(payload);
|
|
673
|
+
}
|
|
674
|
+
finally {
|
|
675
|
+
this.inflight.delete(runId);
|
|
676
|
+
}
|
|
496
677
|
}
|
|
497
678
|
}
|
|
498
679
|
}
|
package/dist/runtime-env.d.ts
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
export declare function clearAgentServiceControlEnv(env?: NodeJS.ProcessEnv): void;
|
|
3
3
|
/** Copy an environment for a runtime child without leaking Agent Service control values. */
|
|
4
4
|
export declare function runtimeChildEnv(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
5
|
+
/**
|
|
6
|
+
* Validate the short-lived model environment delivered by session.activate.
|
|
7
|
+
* Only model adapter coordinates are accepted; Course/Agent Service control credentials
|
|
8
|
+
* and supervisor settings can never be reintroduced through the wire payload.
|
|
9
|
+
*/
|
|
10
|
+
export declare function activationRuntimeEnv(value: unknown): Record<string, string>;
|
|
5
11
|
/** Run model processes as the Template's untrusted runtime UID when configured. */
|
|
6
12
|
export declare function runtimeChildIdentity(env?: NodeJS.ProcessEnv): {
|
|
7
13
|
uid?: number;
|
package/dist/runtime-env.js
CHANGED
|
@@ -4,8 +4,9 @@ const AGENT_SERVICE_CONTROL_ENV_KEYS = [
|
|
|
4
4
|
"BOTLEARN_AGENT_SERVICE_RUN_TOKEN",
|
|
5
5
|
"BOTLEARN_AGENT_SERVICE_WORKER_ID",
|
|
6
6
|
"BOTLEARN_AGENT_SERVICE_WS_URL",
|
|
7
|
-
"
|
|
8
|
-
"
|
|
7
|
+
"BOTLEARN_AGENT_SERVICE_SANDBOX_ID",
|
|
8
|
+
"BOTLEARN_AGENT_SERVICE_SANDBOX_TOKEN",
|
|
9
|
+
"BOTLEARN_AGENT_SERVICE_ACTIVATION_ID",
|
|
9
10
|
];
|
|
10
11
|
const AGENT_SERVICE_SUPERVISOR_ENV_KEYS = [
|
|
11
12
|
"BOTLEARN_DAEMON_HOME",
|
|
@@ -18,6 +19,10 @@ const AGENT_SERVICE_SUPERVISOR_ENV_KEYS = [
|
|
|
18
19
|
"BOTLEARN_AGENT_SERVICE_WORKSPACE_ROOT",
|
|
19
20
|
"BOTLEARN_AGENT_SERVICE_PROFILE_ROOT",
|
|
20
21
|
];
|
|
22
|
+
const ACTIVATION_RUNTIME_ENV_KEYS = new Set([
|
|
23
|
+
"DEEPSEEK_API_KEY",
|
|
24
|
+
"DEEPSEEK_BASE_URL",
|
|
25
|
+
]);
|
|
21
26
|
const RUNTIME_USER_PATTERN = /^[a-z_][a-z0-9_-]{0,31}$/;
|
|
22
27
|
const RUNTIME_SUDO_BINARY = "/usr/bin/sudo";
|
|
23
28
|
const MANAGED_RUNTIME_LAUNCHER = "/opt/botlearn/bin/botlearn-runtime-launcher";
|
|
@@ -38,6 +43,34 @@ export function runtimeChildEnv(env = process.env) {
|
|
|
38
43
|
childEnv.HOME = runtimeHome;
|
|
39
44
|
return childEnv;
|
|
40
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Validate the short-lived model environment delivered by session.activate.
|
|
48
|
+
* Only model adapter coordinates are accepted; Course/Agent Service control credentials
|
|
49
|
+
* and supervisor settings can never be reintroduced through the wire payload.
|
|
50
|
+
*/
|
|
51
|
+
export function activationRuntimeEnv(value) {
|
|
52
|
+
if (value === undefined || value === null)
|
|
53
|
+
return {};
|
|
54
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
55
|
+
throw new Error("activation runtime_env must be an object");
|
|
56
|
+
}
|
|
57
|
+
const result = {};
|
|
58
|
+
for (const [key, item] of Object.entries(value)) {
|
|
59
|
+
if (!ACTIVATION_RUNTIME_ENV_KEYS.has(key) ||
|
|
60
|
+
typeof item !== "string" ||
|
|
61
|
+
item.length < 1 ||
|
|
62
|
+
item.length > 8192 ||
|
|
63
|
+
item.includes("\0")) {
|
|
64
|
+
throw new Error(`activation runtime env is forbidden: ${key}`);
|
|
65
|
+
}
|
|
66
|
+
result[key] = item;
|
|
67
|
+
}
|
|
68
|
+
if (Object.keys(result).length > 0 &&
|
|
69
|
+
(!result.DEEPSEEK_API_KEY || !result.DEEPSEEK_BASE_URL)) {
|
|
70
|
+
throw new Error("activation runtime env requires the DeepSeek key and base URL pair");
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
41
74
|
/** Run model processes as the Template's untrusted runtime UID when configured. */
|
|
42
75
|
export function runtimeChildIdentity(env = process.env) {
|
|
43
76
|
const uid = Number(env.BOTLEARN_RUNTIME_UID);
|
package/dist/runtime-profile.js
CHANGED
|
@@ -149,7 +149,18 @@ function archiveEntries(skill) {
|
|
|
149
149
|
if (total > MAX_PACKAGE_BYTES) {
|
|
150
150
|
throw new RuntimeProfileApplyError(`Skill ${skill.id} archive is too large`);
|
|
151
151
|
}
|
|
152
|
-
return entries.sort((a, b) => a.path
|
|
152
|
+
return entries.sort((a, b) => compareUnicodeCodePoints(a.path, b.path));
|
|
153
|
+
}
|
|
154
|
+
function compareUnicodeCodePoints(left, right) {
|
|
155
|
+
const leftCodePoints = Array.from(left, (value) => value.codePointAt(0));
|
|
156
|
+
const rightCodePoints = Array.from(right, (value) => value.codePointAt(0));
|
|
157
|
+
const length = Math.min(leftCodePoints.length, rightCodePoints.length);
|
|
158
|
+
for (let index = 0; index < length; index += 1) {
|
|
159
|
+
const difference = leftCodePoints[index] - rightCodePoints[index];
|
|
160
|
+
if (difference !== 0)
|
|
161
|
+
return difference;
|
|
162
|
+
}
|
|
163
|
+
return leftCodePoints.length - rightCodePoints.length;
|
|
153
164
|
}
|
|
154
165
|
function validateArchiveFile(file, skillId) {
|
|
155
166
|
if (!file || typeof file.path !== "string" || typeof file.content !== "string") {
|
|
@@ -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",
|
|
@@ -38,10 +38,11 @@ export declare class DeepseekTuiAdapter implements EngineAdapter {
|
|
|
38
38
|
private resolveBinary;
|
|
39
39
|
private acquireHandle;
|
|
40
40
|
/**
|
|
41
|
-
* 不设置 DEEPSEEK_RUNTIME_DIR:server
|
|
42
|
-
*
|
|
41
|
+
* 不设置 DEEPSEEK_RUNTIME_DIR:BYOA server 可跨 run 池化,直接使用用户本机
|
|
42
|
+
* deepseek 默认状态目录(含已登录凭据);Agent Service server 按 activation 回收。
|
|
43
43
|
*/
|
|
44
44
|
private spawnEnv;
|
|
45
|
+
private managedActivationId;
|
|
45
46
|
private createThread;
|
|
46
47
|
private patchThreadSystemContext;
|
|
47
48
|
private startTurnAndReadEvents;
|
|
@@ -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;
|
|
@@ -99,6 +111,7 @@ export class DeepseekTuiAdapter {
|
|
|
99
111
|
let handle;
|
|
100
112
|
let countedInFlight = false;
|
|
101
113
|
let releaseTurn;
|
|
114
|
+
const managedActivationId = this.managedActivationId(opts);
|
|
102
115
|
try {
|
|
103
116
|
// The local server has a process-level kill fallback when turn-scoped interrupt
|
|
104
117
|
// fails. Serialize turns so cancelling one run can never terminate another run.
|
|
@@ -114,7 +127,10 @@ export class DeepseekTuiAdapter {
|
|
|
114
127
|
if (handle.idleTimer)
|
|
115
128
|
clearTimeout(handle.idleTimer);
|
|
116
129
|
const headers = authHeaders(handle.token);
|
|
117
|
-
|
|
130
|
+
// Agent Service model credentials are activation-scoped. The local DeepSeek
|
|
131
|
+
// server reads them only at process startup, so its native thread cache cannot
|
|
132
|
+
// safely cross activations; durable Course context rebuilds the new thread.
|
|
133
|
+
let threadId = managedActivationId ? "" : (opts.sessionId?.trim() || "");
|
|
118
134
|
if (threadId && !isValidThreadId(threadId)) {
|
|
119
135
|
return {
|
|
120
136
|
text: "",
|
|
@@ -140,7 +156,7 @@ export class DeepseekTuiAdapter {
|
|
|
140
156
|
const error = runResult.error ?? (text === "" ? emptyCompletionError(handle.stderrTail) : undefined);
|
|
141
157
|
return {
|
|
142
158
|
text,
|
|
143
|
-
newSessionId: threadId,
|
|
159
|
+
newSessionId: managedActivationId ? "" : threadId,
|
|
144
160
|
...(runResult.progressDispositions
|
|
145
161
|
? { progressDispositions: runResult.progressDispositions }
|
|
146
162
|
: {}),
|
|
@@ -150,11 +166,11 @@ export class DeepseekTuiAdapter {
|
|
|
150
166
|
}
|
|
151
167
|
catch (err) {
|
|
152
168
|
const message = err instanceof Error ? err.message : String(err);
|
|
153
|
-
//
|
|
154
|
-
const staleSession = opts.sessionId &&
|
|
169
|
+
// 服务端明确确认线程不存在/已过期时才清空 sessionId,让下一轮从 durable context 重建。
|
|
170
|
+
const staleSession = Boolean(opts.sessionId) && isMissingThreadHttpError(err);
|
|
155
171
|
return {
|
|
156
172
|
text: "",
|
|
157
|
-
newSessionId: staleSession ? "" : (opts.sessionId ?? ""),
|
|
173
|
+
newSessionId: managedActivationId || staleSession ? "" : (opts.sessionId ?? ""),
|
|
158
174
|
error: `deepseek-tui: ${message}`,
|
|
159
175
|
};
|
|
160
176
|
}
|
|
@@ -162,8 +178,14 @@ export class DeepseekTuiAdapter {
|
|
|
162
178
|
opts.signal.removeEventListener("abort", onAbort);
|
|
163
179
|
if (handle && countedInFlight) {
|
|
164
180
|
handle.inFlight = Math.max(0, handle.inFlight - 1);
|
|
165
|
-
if (!this.explicitServerUrl)
|
|
181
|
+
if (managedActivationId && !this.explicitServerUrl && handle.inFlight === 0) {
|
|
182
|
+
if (PROCESS_POOL.get(POOL_KEY) === handle)
|
|
183
|
+
PROCESS_POOL.delete(POOL_KEY);
|
|
184
|
+
shutdownHandle(handle, "managed-activation-finished");
|
|
185
|
+
}
|
|
186
|
+
else if (!this.explicitServerUrl) {
|
|
166
187
|
resetIdle(handle, POOL_KEY);
|
|
188
|
+
}
|
|
167
189
|
}
|
|
168
190
|
releaseTurn?.();
|
|
169
191
|
}
|
|
@@ -182,14 +204,23 @@ export class DeepseekTuiAdapter {
|
|
|
182
204
|
child: nullChild(),
|
|
183
205
|
baseUrl: trimTrailingSlash(this.explicitServerUrl),
|
|
184
206
|
token: this.explicitAuthToken ?? "",
|
|
207
|
+
managedActivationId: null,
|
|
185
208
|
closed: false,
|
|
186
209
|
inFlight: 0,
|
|
187
210
|
stderrTail: "",
|
|
188
211
|
};
|
|
189
212
|
}
|
|
213
|
+
const managedActivationId = this.managedActivationId(opts);
|
|
190
214
|
const existing = PROCESS_POOL.get(POOL_KEY);
|
|
191
|
-
if (existing
|
|
215
|
+
if (existing
|
|
216
|
+
&& !existing.closed
|
|
217
|
+
&& existing.managedActivationId === managedActivationId) {
|
|
192
218
|
return existing;
|
|
219
|
+
}
|
|
220
|
+
if (existing) {
|
|
221
|
+
PROCESS_POOL.delete(POOL_KEY);
|
|
222
|
+
shutdownHandle(existing, "activation-scope-changed");
|
|
223
|
+
}
|
|
193
224
|
const port = await findFreePort();
|
|
194
225
|
if (signal.aborted)
|
|
195
226
|
throw abortReason(signal);
|
|
@@ -231,6 +262,7 @@ export class DeepseekTuiAdapter {
|
|
|
231
262
|
child,
|
|
232
263
|
baseUrl,
|
|
233
264
|
token,
|
|
265
|
+
managedActivationId,
|
|
234
266
|
closed: false,
|
|
235
267
|
inFlight: 0,
|
|
236
268
|
stderrTail: "",
|
|
@@ -266,8 +298,8 @@ export class DeepseekTuiAdapter {
|
|
|
266
298
|
return handle;
|
|
267
299
|
}
|
|
268
300
|
/**
|
|
269
|
-
* 不设置 DEEPSEEK_RUNTIME_DIR:server
|
|
270
|
-
*
|
|
301
|
+
* 不设置 DEEPSEEK_RUNTIME_DIR:BYOA server 可跨 run 池化,直接使用用户本机
|
|
302
|
+
* deepseek 默认状态目录(含已登录凭据);Agent Service server 按 activation 回收。
|
|
271
303
|
*/
|
|
272
304
|
spawnEnv(opts, progressMcpConfigPath) {
|
|
273
305
|
const env = {
|
|
@@ -279,6 +311,12 @@ export class DeepseekTuiAdapter {
|
|
|
279
311
|
env.DEEPSEEK_MCP_CONFIG = progressMcpConfigPath;
|
|
280
312
|
return env;
|
|
281
313
|
}
|
|
314
|
+
managedActivationId(opts) {
|
|
315
|
+
if (this.explicitServerUrl)
|
|
316
|
+
return null;
|
|
317
|
+
const value = opts.env?.BOTLEARN_AGENT_SERVICE_ACTIVATION_ID?.trim();
|
|
318
|
+
return value || null;
|
|
319
|
+
}
|
|
282
320
|
async createThread(baseUrl, headers, opts, signal) {
|
|
283
321
|
const body = {
|
|
284
322
|
workspace: opts.cwd,
|
|
@@ -404,7 +442,7 @@ export class DeepseekTuiAdapter {
|
|
|
404
442
|
async readEvents(url, headers, opts, signal) {
|
|
405
443
|
const res = await this.fetchFn(url, { method: "GET", headers, signal });
|
|
406
444
|
if (!res.ok)
|
|
407
|
-
throw new
|
|
445
|
+
throw new DeepseekHttpError(res.status, "", "events stream failed");
|
|
408
446
|
if (!res.body)
|
|
409
447
|
throw new Error("events stream response missing body");
|
|
410
448
|
const reader = res.body.getReader();
|
|
@@ -538,7 +576,7 @@ export class DeepseekTuiAdapter {
|
|
|
538
576
|
catch {
|
|
539
577
|
// ignore
|
|
540
578
|
}
|
|
541
|
-
throw new
|
|
579
|
+
throw new DeepseekHttpError(res.status, detail);
|
|
542
580
|
}
|
|
543
581
|
return (await res.json());
|
|
544
582
|
}
|
|
@@ -553,16 +591,37 @@ export function __resetDeepseekTuiPoolForTests() {
|
|
|
553
591
|
}
|
|
554
592
|
function normalizeDeepseekEvent(eventName, payload, seq) {
|
|
555
593
|
if (eventName === "message.delta") {
|
|
556
|
-
return {
|
|
594
|
+
return {
|
|
595
|
+
raw: { event: eventName, payload },
|
|
596
|
+
kind: "assistant_text",
|
|
597
|
+
seq,
|
|
598
|
+
text: stringField(payload, "content") ?? "",
|
|
599
|
+
};
|
|
557
600
|
}
|
|
558
601
|
if (eventName === "tool.started" || isToolStarted(eventName, payload)) {
|
|
559
|
-
return {
|
|
602
|
+
return {
|
|
603
|
+
raw: { event: eventName, payload },
|
|
604
|
+
kind: "tool_use",
|
|
605
|
+
seq,
|
|
606
|
+
...(deepseekToolName(payload) ? { name: deepseekToolName(payload) } : {}),
|
|
607
|
+
};
|
|
560
608
|
}
|
|
561
609
|
if (eventName === "tool.completed" || isToolCompleted(eventName, payload)) {
|
|
562
|
-
return {
|
|
610
|
+
return {
|
|
611
|
+
raw: { event: eventName, payload },
|
|
612
|
+
kind: "tool_result",
|
|
613
|
+
seq,
|
|
614
|
+
...(deepseekToolName(payload) ? { name: deepseekToolName(payload) } : {}),
|
|
615
|
+
status: deepseekToolFailed(payload) ? "error" : "completed",
|
|
616
|
+
};
|
|
563
617
|
}
|
|
564
618
|
if (eventName === "item.delta" && isAgentMessageDelta(payload)) {
|
|
565
|
-
return {
|
|
619
|
+
return {
|
|
620
|
+
raw: { event: eventName, payload },
|
|
621
|
+
kind: "assistant_text",
|
|
622
|
+
seq,
|
|
623
|
+
text: extractDeepseekDelta(payload),
|
|
624
|
+
};
|
|
566
625
|
}
|
|
567
626
|
if (eventName === "item.completed" && isAgentReasoningItem(payload)) {
|
|
568
627
|
return { raw: { event: eventName, payload }, kind: "thinking", seq };
|
|
@@ -674,6 +733,19 @@ function inferDeepseekToolName(item) {
|
|
|
674
733
|
}
|
|
675
734
|
return undefined;
|
|
676
735
|
}
|
|
736
|
+
function deepseekToolName(payload) {
|
|
737
|
+
return (stringField(payload, "name")
|
|
738
|
+
?? stringField(payload?.tool, "name")
|
|
739
|
+
?? stringField(payload?.payload?.tool, "name")
|
|
740
|
+
?? inferDeepseekToolName(payload?.item ?? payload?.payload?.item));
|
|
741
|
+
}
|
|
742
|
+
function deepseekToolFailed(payload) {
|
|
743
|
+
const status = (stringField(payload, "status")
|
|
744
|
+
?? stringField(payload?.tool, "status")
|
|
745
|
+
?? stringField(payload?.payload?.tool, "status")
|
|
746
|
+
?? "").toLowerCase();
|
|
747
|
+
return status.includes("fail") || status.includes("error");
|
|
748
|
+
}
|
|
677
749
|
function emptyCompletionError(stderrTail) {
|
|
678
750
|
const tail = stderrTail.trim();
|
|
679
751
|
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 {
|