@botlearn-course/daemon 0.0.13-beta.1 → 0.0.13
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/dist/agent-service-sandbox.d.ts +1 -0
- package/dist/agent-service-sandbox.js +7 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +7 -0
- package/dist/run-dispatcher.d.ts +1 -0
- package/dist/run-dispatcher.js +3 -0
- package/dist/runtime-env.js +2 -0
- package/dist/runtimes/deepseek-tui.d.ts +9 -2
- package/dist/runtimes/deepseek-tui.js +238 -25
- package/dist/runtimes/engine.d.ts +4 -0
- package/dist/runtimes/engine.js +6 -4
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
|
@@ -313,6 +313,7 @@ export class AgentServiceSandboxClient {
|
|
|
313
313
|
return {
|
|
314
314
|
workspaceDir: prepared.workspaceDir,
|
|
315
315
|
transcriptFile: prepared.transcriptFile,
|
|
316
|
+
runtimeStateDir: prepared.rootDir,
|
|
316
317
|
nativeSessionId: session.nativeSessionId,
|
|
317
318
|
contextRevision,
|
|
318
319
|
runtimeEnv,
|
|
@@ -678,6 +679,12 @@ export class AgentServiceSandboxClient {
|
|
|
678
679
|
await this.sendControlFrame("sandbox.ready", {
|
|
679
680
|
protocol_versions: [AGENT_SERVICE_WS_SCHEMA],
|
|
680
681
|
daemon_version: this.options.daemonVersion,
|
|
682
|
+
runtime_versions: {
|
|
683
|
+
course_daemon: this.options.daemonVersion,
|
|
684
|
+
...(this.options.deepseekTuiVersion
|
|
685
|
+
? { deepseek_tui: this.options.deepseekTuiVersion }
|
|
686
|
+
: {}),
|
|
687
|
+
},
|
|
681
688
|
resumed_sessions: Object.keys(this.state.sessions),
|
|
682
689
|
spool_frames: this.spoolFrameCount(),
|
|
683
690
|
});
|
package/dist/cli.d.ts
CHANGED
|
@@ -20,4 +20,5 @@ export interface PollLoopDeps {
|
|
|
20
20
|
log?: Logger;
|
|
21
21
|
}
|
|
22
22
|
export declare function runPollLoop(deps: PollLoopDeps): Promise<PollLoopExit>;
|
|
23
|
+
export declare function assertManagedCourseDaemonVersion(actualVersion: string, selectedVersion: string | undefined): void;
|
|
23
24
|
export declare function runCli(argv: string[]): Promise<number>;
|
package/dist/cli.js
CHANGED
|
@@ -337,6 +337,11 @@ async function readSandboxBootstrapFromStdin() {
|
|
|
337
337
|
chunk.fill(0);
|
|
338
338
|
}
|
|
339
339
|
}
|
|
340
|
+
export function assertManagedCourseDaemonVersion(actualVersion, selectedVersion) {
|
|
341
|
+
if (selectedVersion && selectedVersion !== actualVersion) {
|
|
342
|
+
throw new Error("Managed Course Daemon version does not match the selected release");
|
|
343
|
+
}
|
|
344
|
+
}
|
|
340
345
|
async function cmdAgentServiceSession(args) {
|
|
341
346
|
const bootstrap = args.flags["bootstrap-stdin"] === true
|
|
342
347
|
? await readSandboxBootstrapFromStdin()
|
|
@@ -350,6 +355,7 @@ async function cmdAgentServiceSession(args) {
|
|
|
350
355
|
console.error("Agent Service sandbox environment is incomplete");
|
|
351
356
|
return 1;
|
|
352
357
|
}
|
|
358
|
+
assertManagedCourseDaemonVersion(pkg.version, process.env.BOTLEARN_MANAGED_COURSE_DAEMON_VERSION?.trim() || undefined);
|
|
353
359
|
augmentProcessPath();
|
|
354
360
|
const runtimes = new Map();
|
|
355
361
|
for (const mod of RUNTIME_MODULES) {
|
|
@@ -363,6 +369,7 @@ async function cmdAgentServiceSession(args) {
|
|
|
363
369
|
sandboxToken,
|
|
364
370
|
runtimes,
|
|
365
371
|
daemonVersion: pkg.version,
|
|
372
|
+
deepseekTuiVersion: process.env.BOTLEARN_MANAGED_DEEPSEEK_TUI_VERSION?.trim() || undefined,
|
|
366
373
|
});
|
|
367
374
|
clearAgentServiceControlEnv();
|
|
368
375
|
await client.run();
|
package/dist/run-dispatcher.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface RunDispatcherOptions {
|
|
|
12
12
|
export interface PreparedPersistentTurn {
|
|
13
13
|
workspaceDir: string;
|
|
14
14
|
transcriptFile: string;
|
|
15
|
+
runtimeStateDir?: string;
|
|
15
16
|
nativeSessionId: string | null;
|
|
16
17
|
contextRevision: number;
|
|
17
18
|
runtimeEnv?: NodeJS.ProcessEnv;
|
package/dist/run-dispatcher.js
CHANGED
|
@@ -685,6 +685,9 @@ export class RunDispatcher {
|
|
|
685
685
|
inputAttachments,
|
|
686
686
|
...(persistentTurn
|
|
687
687
|
? {
|
|
688
|
+
...(persistentTurn.runtimeStateDir
|
|
689
|
+
? { runtimeStateDir: persistentTurn.runtimeStateDir }
|
|
690
|
+
: {}),
|
|
688
691
|
nativeSessionId: persistentTurn.nativeSessionId,
|
|
689
692
|
contextRevision: persistentTurn.contextRevision,
|
|
690
693
|
...(persistentTurn.runtimeEnv
|
package/dist/runtime-env.js
CHANGED
|
@@ -17,6 +17,8 @@ const AGENT_SERVICE_SUPERVISOR_ENV_KEYS = [
|
|
|
17
17
|
"BOTLEARN_RUNTIME_HOME",
|
|
18
18
|
"BOTLEARN_RUNTIME_LAUNCHER",
|
|
19
19
|
"BOTLEARN_RUNTIME_LAUNCH_MODE",
|
|
20
|
+
"BOTLEARN_MANAGED_COURSE_DAEMON_VERSION",
|
|
21
|
+
"BOTLEARN_MANAGED_DEEPSEEK_TUI_VERSION",
|
|
20
22
|
"BOTLEARN_AGENT_SERVICE_WORKSPACE_ROOT",
|
|
21
23
|
"BOTLEARN_AGENT_SERVICE_PROFILE_ROOT",
|
|
22
24
|
];
|
|
@@ -38,13 +38,20 @@ export declare class DeepseekTuiAdapter implements EngineAdapter {
|
|
|
38
38
|
private resolveBinary;
|
|
39
39
|
private acquireHandle;
|
|
40
40
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
41
|
+
* BYOA continues to use the user's default DeepSeek state directory. Managed mode
|
|
42
|
+
* pins the durable RuntimeThreadStore to the RuntimeSession workspace even though
|
|
43
|
+
* the credential-bearing server process is still reclaimed after every activation.
|
|
43
44
|
*/
|
|
44
45
|
private spawnEnv;
|
|
46
|
+
private prepareManagedRuntimeDir;
|
|
47
|
+
private compactionMarkerPath;
|
|
48
|
+
private compactionRequired;
|
|
49
|
+
private markCompactionRequired;
|
|
50
|
+
private clearCompactionRequired;
|
|
45
51
|
private managedActivationId;
|
|
46
52
|
private createThread;
|
|
47
53
|
private patchThreadSystemContext;
|
|
54
|
+
private compactThread;
|
|
48
55
|
private startTurnAndReadEvents;
|
|
49
56
|
private interruptTurn;
|
|
50
57
|
private readEvents;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { chmodSync, existsSync, realpathSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, realpathSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import net from "node:net";
|
|
5
5
|
import { MAX_PROGRESS_EVENTS_PER_ATTEMPT } from "../mcp/report-progress.js";
|
|
@@ -27,6 +27,12 @@ const STARTUP_POLL_MS = 250;
|
|
|
27
27
|
/** 单轮流式 assistant 文本字节上限。 */
|
|
28
28
|
const SSE_TEXT_CAP = 1 * 1024 * 1024;
|
|
29
29
|
const VISION_MODEL_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,159}$/;
|
|
30
|
+
const MANAGED_RUNTIME_DIR_NAME = ".botlearn-deepseek-runtime";
|
|
31
|
+
const COMPACTION_REQUIRED_MARKER = "deepseek-native-compaction-required";
|
|
32
|
+
const DEFAULT_DEEPSEEK_CONTEXT_WINDOW_TOKENS = 1_000_000;
|
|
33
|
+
const LEGACY_DEEPSEEK_CONTEXT_WINDOW_TOKENS = 128_000;
|
|
34
|
+
const UNKNOWN_MODEL_COMPACTION_THRESHOLD_TOKENS = 102_400;
|
|
35
|
+
const COMPACTION_THRESHOLD_PERCENT = 80;
|
|
30
36
|
function createManagedVisionConfig(opts, progressMcpConfig) {
|
|
31
37
|
const model = opts.env?.BOTLEARN_DEEPSEEK_VISION_MODEL?.trim();
|
|
32
38
|
if (!model)
|
|
@@ -161,6 +167,8 @@ export class DeepseekTuiAdapter {
|
|
|
161
167
|
let countedInFlight = false;
|
|
162
168
|
let releaseTurn;
|
|
163
169
|
const managedActivationId = this.managedActivationId(opts);
|
|
170
|
+
let threadId = opts.sessionId?.trim() || "";
|
|
171
|
+
let fallbackUsed = false;
|
|
164
172
|
try {
|
|
165
173
|
// The local server has a process-level kill fallback when turn-scoped interrupt
|
|
166
174
|
// fails. Serialize turns so cancelling one run can never terminate another run.
|
|
@@ -176,10 +184,6 @@ export class DeepseekTuiAdapter {
|
|
|
176
184
|
if (handle.idleTimer)
|
|
177
185
|
clearTimeout(handle.idleTimer);
|
|
178
186
|
const headers = authHeaders(handle.token);
|
|
179
|
-
// Agent Service model credentials are activation-scoped. The local DeepSeek
|
|
180
|
-
// server reads them only at process startup, so its native thread cache cannot
|
|
181
|
-
// safely cross activations; durable Course context rebuilds the new thread.
|
|
182
|
-
let threadId = managedActivationId ? "" : (opts.sessionId?.trim() || "");
|
|
183
187
|
if (threadId && !isValidThreadId(threadId)) {
|
|
184
188
|
return {
|
|
185
189
|
text: "",
|
|
@@ -187,39 +191,93 @@ export class DeepseekTuiAdapter {
|
|
|
187
191
|
error: "deepseek-tui: invalid sessionId",
|
|
188
192
|
};
|
|
189
193
|
}
|
|
190
|
-
|
|
191
|
-
|
|
194
|
+
const resumedThreadId = threadId;
|
|
195
|
+
let turnOpts = opts;
|
|
196
|
+
let runResult;
|
|
197
|
+
let maintenanceUsage;
|
|
198
|
+
while (true) {
|
|
199
|
+
try {
|
|
200
|
+
if (!threadId) {
|
|
201
|
+
threadId = await this.createThread(handle.baseUrl, headers, opts, turnAbort.signal);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
if (opts.systemContext !== undefined) {
|
|
205
|
+
await this.patchThreadSystemContext(handle.baseUrl, headers, threadId, opts.systemContext, turnAbort.signal);
|
|
206
|
+
}
|
|
207
|
+
if (managedActivationId && this.compactionRequired(opts)) {
|
|
208
|
+
const compacted = await this.compactThread({
|
|
209
|
+
baseUrl: handle.baseUrl,
|
|
210
|
+
headers,
|
|
211
|
+
threadId,
|
|
212
|
+
opts,
|
|
213
|
+
signal: turnAbort.signal,
|
|
214
|
+
handle,
|
|
215
|
+
});
|
|
216
|
+
maintenanceUsage = compacted.usage;
|
|
217
|
+
if (compacted.error) {
|
|
218
|
+
throw new Error(`native compaction failed: ${compacted.error}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
runResult = await this.startTurnAndReadEvents({
|
|
223
|
+
baseUrl: handle.baseUrl,
|
|
224
|
+
headers,
|
|
225
|
+
threadId,
|
|
226
|
+
opts: turnOpts,
|
|
227
|
+
signal: turnAbort.signal,
|
|
228
|
+
handle,
|
|
229
|
+
});
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
// A persisted native thread is a recoverable cache over the authoritative Course
|
|
234
|
+
// conversation. Rebuild it in the same learner turn only when DeepSeek explicitly
|
|
235
|
+
// confirms that the old thread is missing or expired.
|
|
236
|
+
if (!fallbackUsed
|
|
237
|
+
&& Boolean(resumedThreadId)
|
|
238
|
+
&& threadId === resumedThreadId
|
|
239
|
+
&& isMissingThreadHttpError(error)) {
|
|
240
|
+
fallbackUsed = true;
|
|
241
|
+
threadId = "";
|
|
242
|
+
maintenanceUsage = undefined;
|
|
243
|
+
if (managedActivationId)
|
|
244
|
+
this.clearCompactionRequired(opts);
|
|
245
|
+
turnOpts = {
|
|
246
|
+
...opts,
|
|
247
|
+
text: opts.recoveryText ?? opts.text,
|
|
248
|
+
};
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
192
253
|
}
|
|
193
|
-
|
|
194
|
-
|
|
254
|
+
if (managedActivationId
|
|
255
|
+
&& runResult.usage?.input_tokens !== undefined
|
|
256
|
+
&& runResult.usage.input_tokens >= deepseekCompactionThreshold(runResult.model ?? parseDeepseekRuntimeSelection(opts.extraArgs).model)) {
|
|
257
|
+
this.markCompactionRequired(opts);
|
|
195
258
|
}
|
|
196
|
-
const runResult = await this.startTurnAndReadEvents({
|
|
197
|
-
baseUrl: handle.baseUrl,
|
|
198
|
-
headers,
|
|
199
|
-
threadId,
|
|
200
|
-
opts,
|
|
201
|
-
signal: turnAbort.signal,
|
|
202
|
-
handle,
|
|
203
|
-
});
|
|
204
259
|
const text = runResult.text;
|
|
205
260
|
const error = runResult.error ?? (text === "" ? emptyCompletionError(handle.stderrTail) : undefined);
|
|
261
|
+
const usage = mergeRuntimeUsage(maintenanceUsage, runResult.usage);
|
|
206
262
|
return {
|
|
207
263
|
text,
|
|
208
|
-
newSessionId:
|
|
264
|
+
newSessionId: threadId,
|
|
209
265
|
...(runResult.progressDispositions
|
|
210
266
|
? { progressDispositions: runResult.progressDispositions }
|
|
211
267
|
: {}),
|
|
212
|
-
...(
|
|
268
|
+
...(usage ? { usage } : {}),
|
|
213
269
|
...(error ? { error } : {}),
|
|
214
270
|
};
|
|
215
271
|
}
|
|
216
272
|
catch (err) {
|
|
217
273
|
const message = err instanceof Error ? err.message : String(err);
|
|
218
|
-
//
|
|
219
|
-
const staleSession = Boolean(
|
|
274
|
+
// Only an explicit 404/410 discards the native id. Transient failures preserve it.
|
|
275
|
+
const staleSession = Boolean(threadId) && isMissingThreadHttpError(err);
|
|
220
276
|
return {
|
|
221
277
|
text: "",
|
|
222
|
-
newSessionId:
|
|
278
|
+
newSessionId: staleSession || (fallbackUsed && !threadId)
|
|
279
|
+
? ""
|
|
280
|
+
: (threadId || opts.sessionId || ""),
|
|
223
281
|
error: `deepseek-tui: ${message}`,
|
|
224
282
|
};
|
|
225
283
|
}
|
|
@@ -355,8 +413,9 @@ export class DeepseekTuiAdapter {
|
|
|
355
413
|
return handle;
|
|
356
414
|
}
|
|
357
415
|
/**
|
|
358
|
-
*
|
|
359
|
-
*
|
|
416
|
+
* BYOA continues to use the user's default DeepSeek state directory. Managed mode
|
|
417
|
+
* pins the durable RuntimeThreadStore to the RuntimeSession workspace even though
|
|
418
|
+
* the credential-bearing server process is still reclaimed after every activation.
|
|
360
419
|
*/
|
|
361
420
|
spawnEnv(opts, progressMcpConfigPath, visionConfigPath) {
|
|
362
421
|
const env = {
|
|
@@ -368,8 +427,40 @@ export class DeepseekTuiAdapter {
|
|
|
368
427
|
env.DEEPSEEK_MCP_CONFIG = progressMcpConfigPath;
|
|
369
428
|
if (visionConfigPath)
|
|
370
429
|
env.DEEPSEEK_CONFIG_PATH = visionConfigPath;
|
|
430
|
+
if (this.managedActivationId(opts)) {
|
|
431
|
+
env.DEEPSEEK_RUNTIME_DIR = this.prepareManagedRuntimeDir(opts);
|
|
432
|
+
}
|
|
371
433
|
return env;
|
|
372
434
|
}
|
|
435
|
+
prepareManagedRuntimeDir(opts) {
|
|
436
|
+
const runtimeDir = path.join(opts.cwd, MANAGED_RUNTIME_DIR_NAME);
|
|
437
|
+
mkdirSync(runtimeDir, { recursive: true, mode: 0o770 });
|
|
438
|
+
const stat = lstatSync(runtimeDir);
|
|
439
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
440
|
+
throw new Error("deepseek-tui: managed runtime directory is not a real directory");
|
|
441
|
+
}
|
|
442
|
+
// The control process owns the directory; the fixed runtime group needs write access.
|
|
443
|
+
chmodSync(runtimeDir, 0o770);
|
|
444
|
+
return runtimeDir;
|
|
445
|
+
}
|
|
446
|
+
compactionMarkerPath(opts) {
|
|
447
|
+
if (!opts.runtimeStateDir) {
|
|
448
|
+
throw new Error("deepseek-tui: managed RuntimeSession state directory is unavailable");
|
|
449
|
+
}
|
|
450
|
+
return path.join(opts.runtimeStateDir, COMPACTION_REQUIRED_MARKER);
|
|
451
|
+
}
|
|
452
|
+
compactionRequired(opts) {
|
|
453
|
+
return existsSync(this.compactionMarkerPath(opts));
|
|
454
|
+
}
|
|
455
|
+
markCompactionRequired(opts) {
|
|
456
|
+
writeFileSync(this.compactionMarkerPath(opts), "required\n", {
|
|
457
|
+
encoding: "utf8",
|
|
458
|
+
mode: 0o600,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
clearCompactionRequired(opts) {
|
|
462
|
+
rmSync(this.compactionMarkerPath(opts), { force: true });
|
|
463
|
+
}
|
|
373
464
|
managedActivationId(opts) {
|
|
374
465
|
if (this.explicitServerUrl)
|
|
375
466
|
return null;
|
|
@@ -418,6 +509,74 @@ export class DeepseekTuiAdapter {
|
|
|
418
509
|
signal,
|
|
419
510
|
});
|
|
420
511
|
}
|
|
512
|
+
async compactThread(args) {
|
|
513
|
+
const { baseUrl, headers, threadId, opts, signal, handle } = args;
|
|
514
|
+
const eventsUrl = `${baseUrl}/v1/threads/${encodeURIComponent(threadId)}/events?since_seq=0`;
|
|
515
|
+
const eventsAbort = new AbortController();
|
|
516
|
+
let turnId = "";
|
|
517
|
+
let interruptPromise;
|
|
518
|
+
const interrupt = () => {
|
|
519
|
+
if (!turnId)
|
|
520
|
+
return Promise.resolve();
|
|
521
|
+
interruptPromise ??= this.interruptTurn(baseUrl, headers, threadId, turnId).catch((error) => {
|
|
522
|
+
log.warn("deepseek-tui compaction interrupt failed", {
|
|
523
|
+
error: error instanceof Error ? error.message : String(error),
|
|
524
|
+
});
|
|
525
|
+
if (!this.explicitServerUrl)
|
|
526
|
+
shutdownHandle(handle, "compaction-interrupt-failed");
|
|
527
|
+
});
|
|
528
|
+
return interruptPromise;
|
|
529
|
+
};
|
|
530
|
+
const onAbort = () => {
|
|
531
|
+
eventsAbort.abort();
|
|
532
|
+
void interrupt();
|
|
533
|
+
};
|
|
534
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
535
|
+
let eventsError;
|
|
536
|
+
const quietOpts = {
|
|
537
|
+
...opts,
|
|
538
|
+
onBlock: () => undefined,
|
|
539
|
+
onStatus: () => undefined,
|
|
540
|
+
};
|
|
541
|
+
const eventsReaderPromise = this.readEvents(eventsUrl, headers, quietOpts, eventsAbort.signal).catch((error) => {
|
|
542
|
+
eventsError = error;
|
|
543
|
+
return null;
|
|
544
|
+
});
|
|
545
|
+
try {
|
|
546
|
+
if (signal.aborted)
|
|
547
|
+
throw abortReason(signal);
|
|
548
|
+
const started = await this.requestJson(`${baseUrl}/v1/threads/${encodeURIComponent(threadId)}/compact`, {
|
|
549
|
+
method: "POST",
|
|
550
|
+
headers,
|
|
551
|
+
body: JSON.stringify({
|
|
552
|
+
reason: "BotLearn automatic long-context compaction",
|
|
553
|
+
}),
|
|
554
|
+
signal: AbortSignal.timeout(5_000),
|
|
555
|
+
});
|
|
556
|
+
turnId = stringField(started?.turn, "id") ?? stringField(started, "turn_id") ?? "";
|
|
557
|
+
if (signal.aborted)
|
|
558
|
+
await interrupt();
|
|
559
|
+
const eventsReader = await eventsReaderPromise;
|
|
560
|
+
if (!eventsReader)
|
|
561
|
+
throw eventsError ?? new Error("compaction events stream failed");
|
|
562
|
+
const result = await eventsReader(turnId);
|
|
563
|
+
const model = stringField(started?.thread, "model");
|
|
564
|
+
return {
|
|
565
|
+
...result,
|
|
566
|
+
...(model ? { model } : {}),
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
finally {
|
|
570
|
+
if (signal.aborted) {
|
|
571
|
+
if (turnId)
|
|
572
|
+
await interrupt();
|
|
573
|
+
else if (!this.explicitServerUrl)
|
|
574
|
+
shutdownHandle(handle, "cancelled-before-compaction-id");
|
|
575
|
+
}
|
|
576
|
+
eventsAbort.abort();
|
|
577
|
+
signal.removeEventListener("abort", onAbort);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
421
580
|
async startTurnAndReadEvents(args) {
|
|
422
581
|
const { baseUrl, headers, threadId, opts, signal, handle } = args;
|
|
423
582
|
// 事件流必须先于 turn 打开,否则 turn 早期事件会丢。
|
|
@@ -476,7 +635,12 @@ export class DeepseekTuiAdapter {
|
|
|
476
635
|
const eventsReader = await eventsReaderPromise;
|
|
477
636
|
if (!eventsReader)
|
|
478
637
|
throw eventsError ?? new Error("events stream failed");
|
|
479
|
-
|
|
638
|
+
const result = await eventsReader(turnId);
|
|
639
|
+
const model = stringField(started?.thread, "model");
|
|
640
|
+
return {
|
|
641
|
+
...result,
|
|
642
|
+
...(model ? { model } : {}),
|
|
643
|
+
};
|
|
480
644
|
}
|
|
481
645
|
finally {
|
|
482
646
|
if (signal.aborted) {
|
|
@@ -754,6 +918,55 @@ export function extractDeepseekUsage(payload) {
|
|
|
754
918
|
...(requestId ? { provider_request_ids: [requestId] } : {}),
|
|
755
919
|
};
|
|
756
920
|
}
|
|
921
|
+
/** Mirrors DeepSeek TUI 0.8.39's model-window compaction threshold selection. */
|
|
922
|
+
function deepseekCompactionThreshold(model) {
|
|
923
|
+
if (!model)
|
|
924
|
+
return UNKNOWN_MODEL_COMPACTION_THRESHOLD_TOKENS;
|
|
925
|
+
const lower = model.toLowerCase();
|
|
926
|
+
let contextWindow;
|
|
927
|
+
if (lower.includes("deepseek")) {
|
|
928
|
+
const explicit = lower.match(/(?:^|[^a-z0-9])(\d{1,4})k(?:$|[^a-z0-9])/);
|
|
929
|
+
const kiloTokens = explicit?.[1] ? Number(explicit[1]) : Number.NaN;
|
|
930
|
+
if (Number.isInteger(kiloTokens) && kiloTokens >= 8 && kiloTokens <= 1024) {
|
|
931
|
+
contextWindow = kiloTokens * 1_000;
|
|
932
|
+
}
|
|
933
|
+
else {
|
|
934
|
+
contextWindow = lower.includes("v4")
|
|
935
|
+
? DEFAULT_DEEPSEEK_CONTEXT_WINDOW_TOKENS
|
|
936
|
+
: LEGACY_DEEPSEEK_CONTEXT_WINDOW_TOKENS;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
else if (lower.includes("claude")) {
|
|
940
|
+
contextWindow = 200_000;
|
|
941
|
+
}
|
|
942
|
+
if (contextWindow === undefined)
|
|
943
|
+
return UNKNOWN_MODEL_COMPACTION_THRESHOLD_TOKENS;
|
|
944
|
+
return Math.floor((contextWindow * COMPACTION_THRESHOLD_PERCENT) / 100);
|
|
945
|
+
}
|
|
946
|
+
function mergeRuntimeUsage(first, second) {
|
|
947
|
+
if (!first)
|
|
948
|
+
return second;
|
|
949
|
+
if (!second)
|
|
950
|
+
return first;
|
|
951
|
+
const sum = (left, right) => left !== undefined || right !== undefined ? (left ?? 0) + (right ?? 0) : undefined;
|
|
952
|
+
const requestIds = [...new Set([
|
|
953
|
+
...(first.provider_request_ids ?? []),
|
|
954
|
+
...(second.provider_request_ids ?? []),
|
|
955
|
+
])];
|
|
956
|
+
const input = sum(first.input_tokens, second.input_tokens);
|
|
957
|
+
const cached = sum(first.cached_input_tokens, second.cached_input_tokens);
|
|
958
|
+
const output = sum(first.output_tokens, second.output_tokens);
|
|
959
|
+
const total = sum(first.total_tokens, second.total_tokens);
|
|
960
|
+
const cost = sum(first.cost_usd, second.cost_usd);
|
|
961
|
+
return {
|
|
962
|
+
...(input !== undefined ? { input_tokens: input } : {}),
|
|
963
|
+
...(cached !== undefined ? { cached_input_tokens: cached } : {}),
|
|
964
|
+
...(output !== undefined ? { output_tokens: output } : {}),
|
|
965
|
+
...(total !== undefined ? { total_tokens: total } : {}),
|
|
966
|
+
...(cost !== undefined ? { cost_usd: cost } : {}),
|
|
967
|
+
...(requestIds.length > 0 ? { provider_request_ids: requestIds } : {}),
|
|
968
|
+
};
|
|
969
|
+
}
|
|
757
970
|
function isToolStarted(eventName, payload) {
|
|
758
971
|
const itemKind = payload?.payload?.item?.kind ?? payload?.item?.kind;
|
|
759
972
|
return ((eventName === "item.started" &&
|
|
@@ -34,9 +34,13 @@ export type RuntimeStatusEvent = {
|
|
|
34
34
|
};
|
|
35
35
|
export interface EngineRunOptions {
|
|
36
36
|
text: string;
|
|
37
|
+
/** Full durable conversation bootstrap, used only if a resumed native thread is missing. */
|
|
38
|
+
recoveryText?: string;
|
|
37
39
|
/** runtime 原生会话 id(resume 用);null 表示新会话。 */
|
|
38
40
|
sessionId: string | null;
|
|
39
41
|
cwd: string;
|
|
42
|
+
/** Daemon-owned state directory for runtime metadata that the model must not mutate. */
|
|
43
|
+
runtimeStateDir?: string;
|
|
40
44
|
signal: AbortSignal;
|
|
41
45
|
extraArgs?: string[];
|
|
42
46
|
systemContext?: string;
|
package/dist/runtimes/engine.js
CHANGED
|
@@ -152,14 +152,14 @@ export function wrapEngineAdapter(id, engine, opts) {
|
|
|
152
152
|
const payload = run.payload;
|
|
153
153
|
// DeepSeek persists and replays the native thread history. The durable Course Service
|
|
154
154
|
// conversation is recovery/bootstrap data, not a second history to inject on every turn.
|
|
155
|
-
|
|
156
|
-
// credential expires with the turn, so a cached thread id cannot suppress durable context.
|
|
157
|
-
const managedActivation = Boolean(run.runtimeEnv?.BOTLEARN_AGENT_SERVICE_ACTIVATION_ID?.trim());
|
|
158
|
-
const resumesDeepseekThread = id === "deepseek-tui" && !managedActivation && Boolean(run.nativeSessionId?.trim());
|
|
155
|
+
const resumesDeepseekThread = id === "deepseek-tui" && Boolean(run.nativeSessionId?.trim());
|
|
159
156
|
const learnerInput = resumesDeepseekThread
|
|
160
157
|
? renderCurrentLearnerRequest(payload)
|
|
161
158
|
: renderConversationInput(payload);
|
|
162
159
|
const text = renderInputAttachments(run, learnerInput);
|
|
160
|
+
const recoveryText = resumesDeepseekThread
|
|
161
|
+
? renderInputAttachments(run, renderConversationInput(payload))
|
|
162
|
+
: undefined;
|
|
163
163
|
if (!text.trim()) {
|
|
164
164
|
throw new RuntimeExecutionError("empty task brief");
|
|
165
165
|
}
|
|
@@ -190,8 +190,10 @@ export function wrapEngineAdapter(id, engine, opts) {
|
|
|
190
190
|
};
|
|
191
191
|
const result = await engine.run({
|
|
192
192
|
text,
|
|
193
|
+
...(recoveryText !== undefined ? { recoveryText } : {}),
|
|
193
194
|
sessionId: run.nativeSessionId ?? null,
|
|
194
195
|
cwd: run.workspaceDir,
|
|
196
|
+
...(run.runtimeStateDir ? { runtimeStateDir: run.runtimeStateDir } : {}),
|
|
195
197
|
signal,
|
|
196
198
|
...(run.runtimeEnv ? { env: run.runtimeEnv } : {}),
|
|
197
199
|
...(extraArgs.length > 0 ? { extraArgs } : {}),
|
package/dist/types.d.ts
CHANGED
|
@@ -185,6 +185,8 @@ export interface RunExecution {
|
|
|
185
185
|
payload: RunStartPayload;
|
|
186
186
|
/** Runtime cwd. Managed persistent sessions reuse one session-scoped workspace. */
|
|
187
187
|
workspaceDir: string;
|
|
188
|
+
/** Daemon-owned state directory for one managed RuntimeSession; never exposed to the model. */
|
|
189
|
+
runtimeStateDir?: string;
|
|
188
190
|
/** Runtime-native thread/session id to resume; null creates the first native session. */
|
|
189
191
|
nativeSessionId?: string | null;
|
|
190
192
|
/** Monotonic Course Service context revision accepted for this turn. */
|
package/package.json
CHANGED