@botlearn-course/daemon 0.0.13-beta.1 → 0.0.14

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.
@@ -0,0 +1,439 @@
1
+ import { chmodSync, lstatSync, mkdtempSync, rmSync, } from "node:fs";
2
+ import net from "node:net";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
5
+ import { RuntimeSkillProviderError, safeReferencePath, } from "../runtime-skills.js";
6
+ const DEFAULT_PROTOCOL_VERSION = "2024-11-05";
7
+ const MAX_REQUEST_LINE_BYTES = 32 * 1024;
8
+ const MAX_REFERENCE_BYTES_PER_ACTIVATION = 1024 * 1024;
9
+ const TOOLS = [
10
+ {
11
+ name: "list",
12
+ description: "List the immutable BotLearn Course Skills authorized for this activation.",
13
+ inputSchema: {
14
+ type: "object",
15
+ properties: {},
16
+ additionalProperties: false,
17
+ },
18
+ },
19
+ {
20
+ name: "load",
21
+ description: "Load the locked SKILL.md for one authorized Course Skill before following it.",
22
+ inputSchema: {
23
+ type: "object",
24
+ properties: {
25
+ ref: { type: "string", minLength: 5, maxLength: 240 },
26
+ },
27
+ required: ["ref"],
28
+ additionalProperties: false,
29
+ },
30
+ },
31
+ {
32
+ name: "load_reference",
33
+ description: "Load one relative reference path from a Course Skill already loaded in this activation.",
34
+ inputSchema: {
35
+ type: "object",
36
+ properties: {
37
+ ref: { type: "string", minLength: 5, maxLength: 240 },
38
+ path: { type: "string", minLength: 1, maxLength: 240 },
39
+ },
40
+ required: ["ref", "path"],
41
+ additionalProperties: false,
42
+ },
43
+ },
44
+ ];
45
+ export class CourseSkillsMcpRuntime {
46
+ options;
47
+ grantsByRef;
48
+ loadedSkills = new Map();
49
+ loadedReferences = new Map();
50
+ referenceBytes = 0;
51
+ constructor(options) {
52
+ this.options = options;
53
+ this.grantsByRef = new Map(options.prepared.catalog.map((entry) => [entry.ref, entry]));
54
+ }
55
+ async emitAppliedEvents() {
56
+ for (const entry of this.options.prepared.catalog) {
57
+ await this.emit(entry, "applied");
58
+ }
59
+ }
60
+ async handle(request) {
61
+ if (request.id === undefined)
62
+ return null;
63
+ const id = responseId(request.id);
64
+ if (request.jsonrpc !== "2.0" || typeof request.method !== "string") {
65
+ return { jsonrpc: "2.0", id, error: { code: -32600, message: "Invalid Request" } };
66
+ }
67
+ if (request.method === "initialize") {
68
+ const requested = request.params && typeof request.params === "object"
69
+ ? request.params.protocolVersion
70
+ : undefined;
71
+ return {
72
+ jsonrpc: "2.0",
73
+ id,
74
+ result: {
75
+ protocolVersion: typeof requested === "string" && requested.length > 0
76
+ ? requested
77
+ : DEFAULT_PROTOCOL_VERSION,
78
+ capabilities: { tools: {} },
79
+ serverInfo: { name: "botlearn-course-skills", version: "0.1.0" },
80
+ },
81
+ };
82
+ }
83
+ if (request.method === "tools/list") {
84
+ return { jsonrpc: "2.0", id, result: { tools: TOOLS } };
85
+ }
86
+ if (request.method !== "tools/call") {
87
+ return { jsonrpc: "2.0", id, error: { code: -32601, message: "Method not found" } };
88
+ }
89
+ const params = objectValue(request.params);
90
+ const name = params && typeof params.name === "string" ? params.name : "";
91
+ const args = params ? objectValue(params.arguments) : null;
92
+ try {
93
+ if (name === "list") {
94
+ requireExactArguments(args, []);
95
+ return toolSuccess(id, "skill_catalog_listed", {
96
+ skills: this.options.prepared.catalog.map((entry) => ({
97
+ ref: entry.ref,
98
+ digest: entry.digest,
99
+ name: entry.name,
100
+ description: entry.description,
101
+ version: entry.version,
102
+ requiredCapabilities: entry.requiredCapabilities,
103
+ })),
104
+ });
105
+ }
106
+ if (name === "load") {
107
+ requireExactArguments(args, ["ref"]);
108
+ const ref = requiredArgument(args, "ref");
109
+ return toolSuccess(id, "skill_loaded", await this.loadSkill(ref));
110
+ }
111
+ if (name === "load_reference") {
112
+ requireExactArguments(args, ["ref", "path"]);
113
+ const ref = requiredArgument(args, "ref");
114
+ const referencePath = requiredArgument(args, "path");
115
+ return toolSuccess(id, "skill_reference_loaded", await this.loadReference(ref, referencePath));
116
+ }
117
+ return toolFailure(id, "unknown_tool");
118
+ }
119
+ catch (error) {
120
+ return toolFailure(id, safeRuntimeErrorCode(error));
121
+ }
122
+ }
123
+ async loadSkill(ref) {
124
+ const entry = this.requireGrant(ref);
125
+ const cached = this.loadedSkills.get(ref);
126
+ if (cached)
127
+ return cached;
128
+ const pending = (async () => {
129
+ await this.emit(entry, "selected");
130
+ try {
131
+ const loaded = await this.options.prepared.provider.loadSkill(ref);
132
+ await this.emit(entry, "loaded", {
133
+ bytes: loaded.bytes,
134
+ truncated: loaded.truncated,
135
+ });
136
+ return loaded;
137
+ }
138
+ catch (error) {
139
+ await this.emitLoadFailed(entry, error);
140
+ throw error;
141
+ }
142
+ })();
143
+ this.loadedSkills.set(ref, pending);
144
+ try {
145
+ return await pending;
146
+ }
147
+ catch (error) {
148
+ this.loadedSkills.delete(ref);
149
+ throw error;
150
+ }
151
+ }
152
+ async loadReference(ref, referencePath) {
153
+ const entry = this.requireGrant(ref);
154
+ let safePath;
155
+ try {
156
+ safePath = safeReferencePath(referencePath);
157
+ }
158
+ catch (error) {
159
+ await this.emitLoadFailed(entry, error);
160
+ throw error;
161
+ }
162
+ const selected = this.loadedSkills.get(ref);
163
+ if (!selected) {
164
+ const error = new RuntimeSkillProviderError("skill_not_selected", "Load the Skill before loading one of its references");
165
+ await this.emitLoadFailed(entry, error);
166
+ throw error;
167
+ }
168
+ // Presence in the cache only means load(ref) was selected. A parallel MCP
169
+ // request must not read references until the locked main content has
170
+ // actually passed Provider identity validation.
171
+ await selected;
172
+ const key = `${ref}\0${safePath}`;
173
+ const cached = this.loadedReferences.get(key);
174
+ if (cached)
175
+ return cached;
176
+ const pending = (async () => {
177
+ try {
178
+ const loaded = await this.options.prepared.provider.loadReference(ref, safePath);
179
+ if (loaded.bytes > MAX_REFERENCE_BYTES_PER_ACTIVATION - this.referenceBytes) {
180
+ throw new RuntimeSkillProviderError("skill_load_budget_exceeded", "Course Skill reference read budget exceeded");
181
+ }
182
+ this.referenceBytes += loaded.bytes;
183
+ await this.emit(entry, "reference_loaded", {
184
+ bytes: loaded.bytes,
185
+ truncated: loaded.truncated,
186
+ });
187
+ return loaded;
188
+ }
189
+ catch (error) {
190
+ await this.emitLoadFailed(entry, error);
191
+ throw error;
192
+ }
193
+ })();
194
+ this.loadedReferences.set(key, pending);
195
+ try {
196
+ return await pending;
197
+ }
198
+ catch (error) {
199
+ this.loadedReferences.delete(key);
200
+ throw error;
201
+ }
202
+ }
203
+ requireGrant(ref) {
204
+ const entry = this.grantsByRef.get(ref);
205
+ if (!entry) {
206
+ throw new RuntimeSkillProviderError("skill_not_eligible", "Skill is not eligible for the current activation");
207
+ }
208
+ return entry;
209
+ }
210
+ async emit(entry, state, fields = {}) {
211
+ await this.options.onEvent({
212
+ schema_version: "agent-skill-event/0.1",
213
+ kind: "skill_event",
214
+ state,
215
+ ref: entry.ref,
216
+ digest: entry.digest,
217
+ load_mode: "mcp",
218
+ ...fields,
219
+ });
220
+ }
221
+ async emitLoadFailed(entry, error) {
222
+ await this.options.onEvent({
223
+ schema_version: "agent-skill-event/0.1",
224
+ kind: "skill_event",
225
+ state: "load_failed",
226
+ ref: entry.ref,
227
+ digest: entry.digest,
228
+ load_mode: "mcp",
229
+ code: safeRuntimeErrorCode(error),
230
+ });
231
+ }
232
+ }
233
+ export async function startCourseSkillsMcpServer(options) {
234
+ const managedRoot = resolveManagedSkillsRoot(options.managedRoot);
235
+ const dir = mkdtempSync(path.join(managedRoot ?? tmpdir(), "botlearn-skills-mcp-"));
236
+ const socketPath = path.join(dir, "server.sock");
237
+ const runtime = new CourseSkillsMcpRuntime(options);
238
+ const sockets = new Set();
239
+ const server = net.createServer((socket) => {
240
+ sockets.add(socket);
241
+ socket.on("close", () => sockets.delete(socket));
242
+ socket.on("error", () => socket.destroy());
243
+ serveSocket(socket, runtime);
244
+ });
245
+ try {
246
+ if (managedRoot)
247
+ chmodSync(dir, 0o750);
248
+ await listen(server, socketPath);
249
+ chmodSync(socketPath, managedRoot ? 0o660 : 0o600);
250
+ }
251
+ catch (error) {
252
+ await closeServer(server, sockets);
253
+ rmSync(dir, { recursive: true, force: true });
254
+ throw error;
255
+ }
256
+ let closed = false;
257
+ let applied;
258
+ return {
259
+ socketPath,
260
+ catalog: options.prepared.catalog,
261
+ markApplied() {
262
+ applied ??= runtime.emitAppliedEvents();
263
+ return applied;
264
+ },
265
+ async close() {
266
+ if (closed)
267
+ return;
268
+ closed = true;
269
+ await closeServer(server, sockets);
270
+ rmSync(dir, { recursive: true, force: true });
271
+ },
272
+ };
273
+ }
274
+ function serveSocket(socket, runtime) {
275
+ socket.setEncoding("utf8");
276
+ let buffer = "";
277
+ let chain = Promise.resolve();
278
+ socket.on("data", (chunk) => {
279
+ buffer += chunk;
280
+ if (Buffer.byteLength(buffer, "utf8") > MAX_REQUEST_LINE_BYTES) {
281
+ socket.destroy();
282
+ return;
283
+ }
284
+ let newline;
285
+ while ((newline = buffer.indexOf("\n")) >= 0) {
286
+ const line = buffer.slice(0, newline);
287
+ buffer = buffer.slice(newline + 1);
288
+ if (!line.trim())
289
+ continue;
290
+ chain = chain.then(async () => {
291
+ let response;
292
+ try {
293
+ response = await runtime.handle(JSON.parse(line));
294
+ }
295
+ catch {
296
+ response = {
297
+ jsonrpc: "2.0",
298
+ id: null,
299
+ error: { code: -32700, message: "Parse error" },
300
+ };
301
+ }
302
+ if (response && !socket.destroyed) {
303
+ socket.write(`${JSON.stringify(response)}\n`);
304
+ }
305
+ }).catch(() => {
306
+ socket.destroy();
307
+ });
308
+ }
309
+ });
310
+ }
311
+ function responseId(value) {
312
+ return typeof value === "string" || typeof value === "number" || value === null
313
+ ? value
314
+ : null;
315
+ }
316
+ function toolSuccess(id, code, data) {
317
+ return {
318
+ jsonrpc: "2.0",
319
+ id,
320
+ result: {
321
+ content: [{
322
+ type: "text",
323
+ text: JSON.stringify({
324
+ schemaVersion: "tool-result/0.1",
325
+ ok: true,
326
+ code,
327
+ data,
328
+ evidence: [],
329
+ }),
330
+ }],
331
+ isError: false,
332
+ },
333
+ };
334
+ }
335
+ function toolFailure(id, code) {
336
+ return {
337
+ jsonrpc: "2.0",
338
+ id,
339
+ result: {
340
+ content: [{
341
+ type: "text",
342
+ text: JSON.stringify({
343
+ schemaVersion: "tool-result/0.1",
344
+ ok: false,
345
+ code,
346
+ error: { message: "Course Skill request failed" },
347
+ evidence: [],
348
+ }),
349
+ }],
350
+ isError: true,
351
+ },
352
+ };
353
+ }
354
+ function objectValue(value) {
355
+ return value && typeof value === "object" && !Array.isArray(value)
356
+ ? value
357
+ : null;
358
+ }
359
+ function requireExactArguments(args, keys) {
360
+ if (keys.length === 0 && args === null)
361
+ return;
362
+ if (!args || Object.keys(args).length !== keys.length) {
363
+ throw new RuntimeSkillProviderError("skill_request_invalid", "Course Skill tool arguments are invalid");
364
+ }
365
+ const allowed = new Set(keys);
366
+ if (Object.keys(args).some((key) => !allowed.has(key))) {
367
+ throw new RuntimeSkillProviderError("skill_request_invalid", "Course Skill tool arguments are invalid");
368
+ }
369
+ }
370
+ function requiredArgument(args, key) {
371
+ const value = args?.[key];
372
+ if (typeof value !== "string"
373
+ || value.length < 1
374
+ || Buffer.byteLength(value, "utf8") > 240) {
375
+ throw new RuntimeSkillProviderError("skill_request_invalid", "Course Skill tool arguments are invalid");
376
+ }
377
+ return value;
378
+ }
379
+ function safeRuntimeErrorCode(error) {
380
+ if (error instanceof RuntimeSkillProviderError
381
+ && /^[a-z][a-z0-9_]{2,79}$/.test(error.code)) {
382
+ return error.code;
383
+ }
384
+ return "skill_load_failed";
385
+ }
386
+ function resolveManagedSkillsRoot(explicit) {
387
+ if (explicit === null)
388
+ return null;
389
+ let root = explicit?.trim();
390
+ if (root === undefined) {
391
+ const managedRuntime = process.env.BOTLEARN_RUNTIME_USER?.trim()
392
+ || process.env.BOTLEARN_RUNTIME_LAUNCH_MODE?.trim();
393
+ if (!managedRuntime)
394
+ return null;
395
+ root = process.env.BOTLEARN_AGENT_SERVICE_PROFILE_ROOT?.trim();
396
+ if (!root) {
397
+ throw new RuntimeSkillProviderError("skill_provider_runtime_boundary_invalid", "Managed runtime is missing its profile root");
398
+ }
399
+ }
400
+ if (!root || !path.isAbsolute(root)) {
401
+ throw new RuntimeSkillProviderError("skill_provider_runtime_boundary_invalid", "Managed Course Skill root must be absolute");
402
+ }
403
+ try {
404
+ const stat = lstatSync(root);
405
+ const currentUid = process.getuid?.();
406
+ if (!stat.isDirectory()
407
+ || stat.isSymbolicLink()
408
+ || (currentUid !== undefined && stat.uid !== currentUid)
409
+ || (stat.mode & 0o022) !== 0) {
410
+ throw new Error("unsafe root");
411
+ }
412
+ }
413
+ catch {
414
+ throw new RuntimeSkillProviderError("skill_provider_runtime_boundary_invalid", "Managed Course Skill root is unavailable");
415
+ }
416
+ return root;
417
+ }
418
+ function listen(server, socketPath) {
419
+ return new Promise((resolve, reject) => {
420
+ const onError = (error) => {
421
+ server.off("listening", onListening);
422
+ reject(error);
423
+ };
424
+ const onListening = () => {
425
+ server.off("error", onError);
426
+ resolve();
427
+ };
428
+ server.once("error", onError);
429
+ server.once("listening", onListening);
430
+ server.listen(socketPath);
431
+ });
432
+ }
433
+ function closeServer(server, sockets) {
434
+ for (const socket of sockets)
435
+ socket.destroy();
436
+ if (!server.listening)
437
+ return Promise.resolve();
438
+ return new Promise((resolve) => server.close(() => resolve()));
439
+ }
@@ -1,6 +1,7 @@
1
1
  import { type ScanLimits } from "./file-candidates.js";
2
2
  import { type InputAttachmentGrant } from "./input-attachments.js";
3
3
  import { type Logger } from "./log.js";
4
+ import type { PreparedRuntimeSkillProvider } from "./runtime-skills.js";
4
5
  import { type CourseRuntimeProfile, type CourseRuntime, type RunEvent, type RunStartPayload } from "./types.js";
5
6
  export interface RunDispatcherOptions {
6
7
  defaultRuntimeId?: string;
@@ -12,10 +13,12 @@ export interface RunDispatcherOptions {
12
13
  export interface PreparedPersistentTurn {
13
14
  workspaceDir: string;
14
15
  transcriptFile: string;
16
+ runtimeStateDir?: string;
15
17
  nativeSessionId: string | null;
16
18
  contextRevision: number;
17
19
  runtimeEnv?: NodeJS.ProcessEnv;
18
20
  inputAttachmentGrant?: InputAttachmentGrant;
21
+ skillProvider?: PreparedRuntimeSkillProvider;
19
22
  }
20
23
  export interface PersistentSessionExecution {
21
24
  prepareTurn(payload: RunStartPayload): PreparedPersistentTurn;
@@ -9,6 +9,7 @@ import { missingRunCapabilities } from "./runtime-capabilities.js";
9
9
  import { RunQueue } from "./run-queue.js";
10
10
  import { applyRunRuntimeProfile, cleanupRunRuntimeProfile, RuntimeProfileApplyError, runtimeProfileInstructions, } from "./runtime-profile.js";
11
11
  import { TranscriptWriter } from "./transcript.js";
12
+ import { buildToolObservation } from "./tool-observation.js";
12
13
  import { RuntimeExecutionError, } from "./types.js";
13
14
  import { ensureRunWorkspace, transcriptPath } from "./workspace.js";
14
15
  // 与 runtimes/index.ts 的 DEFAULT_RUNTIME_ID 保持一致(此处不 import registry,避免拉入全部 adapter)。
@@ -24,7 +25,8 @@ const CONTENT_FLUSH_INTERVAL_MS = 100;
24
25
  const AGENT_STREAM_SCHEMA_VERSION = "agent-stream/0.1";
25
26
  const SAFE_TOOL_NAME = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,79}$/;
26
27
  const SAFE_FAILURE_CODE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$/;
27
- const FAILURE_DIAGNOSTIC_SCHEMA_VERSION = "botlearn-agent-run-failure/1";
28
+ const SAFE_FAILURE_MODEL = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,159}$/;
29
+ const FAILURE_DIAGNOSTIC_SCHEMA_VERSION = "botlearn-agent-run-failure/2";
28
30
  function wireFailureDiagnostic(runtime, error, message) {
29
31
  const failure = error instanceof RuntimeExecutionError ? error.failure : undefined;
30
32
  const info = errorInfo(error);
@@ -52,6 +54,29 @@ function wireFailureDiagnostic(runtime, error, message) {
52
54
  if (typeof errorName === "string" && SAFE_FAILURE_CODE.test(errorName)) {
53
55
  diagnostic.error_name = errorName;
54
56
  }
57
+ if (typeof failure?.error_code === "string" && SAFE_FAILURE_CODE.test(failure.error_code)) {
58
+ diagnostic.error_code = failure.error_code;
59
+ }
60
+ if (typeof failure?.model === "string" && SAFE_FAILURE_MODEL.test(failure.model)) {
61
+ diagnostic.model = failure.model;
62
+ }
63
+ if (failure?.completion) {
64
+ diagnostic.completion = {
65
+ assistant_message_count: Math.max(0, Math.min(10_000, Math.floor(failure.completion.assistant_message_count))),
66
+ reasoning_message_count: Math.max(0, Math.min(10_000, Math.floor(failure.completion.reasoning_message_count))),
67
+ assistant_content_present: Boolean(failure.completion.assistant_content_present),
68
+ reasoning_content_present: Boolean(failure.completion.reasoning_content_present),
69
+ tool_call_count: Math.max(0, Math.min(10_000, Math.floor(failure.completion.tool_call_count))),
70
+ ...(typeof failure.completion.turn_status === "string"
71
+ && SAFE_FAILURE_CODE.test(failure.completion.turn_status)
72
+ ? { turn_status: failure.completion.turn_status }
73
+ : {}),
74
+ ...(typeof failure.completion.finish_reason === "string"
75
+ && SAFE_FAILURE_CODE.test(failure.completion.finish_reason)
76
+ ? { finish_reason: failure.completion.finish_reason }
77
+ : {}),
78
+ };
79
+ }
55
80
  if (typeof failure?.stderr_tail === "string" && failure.stderr_tail) {
56
81
  diagnostic.stderr_tail = sanitizeRuntimeFailureText(failure.stderr_tail, 8192);
57
82
  }
@@ -207,6 +232,7 @@ export class RunDispatcher {
207
232
  const providerReported = inputTokens !== undefined
208
233
  || runtimeUsage.cached_input_tokens !== undefined
209
234
  || outputTokens !== undefined
235
+ || runtimeUsage.reasoning_tokens !== undefined
210
236
  || runtimeUsage.cost_usd !== undefined;
211
237
  return {
212
238
  schema_version: "agent-run-usage/0.1",
@@ -278,7 +304,6 @@ export class RunDispatcher {
278
304
  agentRunId: runId,
279
305
  traceId,
280
306
  runtime: runtimeId,
281
- model: payload.runtime.model,
282
307
  status: event.type,
283
308
  errorType: event.payload?.error_type,
284
309
  });
@@ -491,6 +516,23 @@ export class RunDispatcher {
491
516
  });
492
517
  };
493
518
  const sink = {
519
+ skillEvent: async (event) => {
520
+ await flushContent();
521
+ await queueStreamEvent({
522
+ type: "run.block",
523
+ payload: {
524
+ schema_version: event.schema_version,
525
+ kind: event.kind,
526
+ state: event.state,
527
+ ref: event.ref,
528
+ digest: event.digest,
529
+ load_mode: event.load_mode,
530
+ ...(event.bytes !== undefined ? { bytes: event.bytes } : {}),
531
+ ...(event.truncated !== undefined ? { truncated: event.truncated } : {}),
532
+ ...(event.code !== undefined ? { code: event.code } : {}),
533
+ },
534
+ });
535
+ },
494
536
  progressDispositions: async (dispositions) => {
495
537
  progressDisposition.invalid += dispositions.invalid;
496
538
  progressDisposition.deduplicated += dispositions.deduplicated;
@@ -622,6 +664,13 @@ export class RunDispatcher {
622
664
  if (block.kind === "tool_call") {
623
665
  lastReportedKind = block.kind;
624
666
  lastReasoningPhase = null;
667
+ const observation = buildToolObservation(block, runtime.id, workspaceDir);
668
+ if (observation) {
669
+ await send({
670
+ type: "run.observation",
671
+ payload: observation,
672
+ });
673
+ }
625
674
  await queueStreamEvent({
626
675
  type: "run.block",
627
676
  payload: {
@@ -639,6 +688,13 @@ export class RunDispatcher {
639
688
  if (block.kind === "tool_result") {
640
689
  lastReportedKind = block.kind;
641
690
  lastReasoningPhase = null;
691
+ const observation = buildToolObservation(block, runtime.id, workspaceDir);
692
+ if (observation) {
693
+ await send({
694
+ type: "run.observation",
695
+ payload: observation,
696
+ });
697
+ }
642
698
  await queueStreamEvent({
643
699
  type: "run.block",
644
700
  payload: {
@@ -685,11 +741,17 @@ export class RunDispatcher {
685
741
  inputAttachments,
686
742
  ...(persistentTurn
687
743
  ? {
744
+ ...(persistentTurn.runtimeStateDir
745
+ ? { runtimeStateDir: persistentTurn.runtimeStateDir }
746
+ : {}),
688
747
  nativeSessionId: persistentTurn.nativeSessionId,
689
748
  contextRevision: persistentTurn.contextRevision,
690
749
  ...(persistentTurn.runtimeEnv
691
750
  ? { runtimeEnv: persistentTurn.runtimeEnv }
692
751
  : {}),
752
+ ...(persistentTurn.skillProvider
753
+ ? { skillProvider: persistentTurn.skillProvider }
754
+ : {}),
693
755
  }
694
756
  : {}),
695
757
  }, sink, controller.signal);
@@ -1,3 +1,4 @@
1
1
  import type { RunStartPayload } from "./types.js";
2
+ export declare function runtimeSupportsCourseSkills(runtimeId: string): boolean;
2
3
  export declare function availableRunCapabilities(payload: RunStartPayload, workspaceDir: string): string[];
3
4
  export declare function missingRunCapabilities(payload: RunStartPayload, workspaceDir: string): string[];
@@ -9,6 +9,10 @@ const WEB_SEARCH_RUNTIMES = new Set([
9
9
  "hermes-agent",
10
10
  "fake",
11
11
  ]);
12
+ const COURSE_SKILL_RUNTIMES = new Set(["deepseek-tui"]);
13
+ export function runtimeSupportsCourseSkills(runtimeId) {
14
+ return COURSE_SKILL_RUNTIMES.has(runtimeId);
15
+ }
12
16
  export function availableRunCapabilities(payload, workspaceDir) {
13
17
  const capabilities = new Set();
14
18
  const probePath = path.join(workspaceDir, `.botlearn-write-probe-${process.pid}-${randomUUID()}`);
@@ -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
  ];