@botlearn-course/daemon 0.0.13 → 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.
- package/README.md +5 -2
- package/dist/agent-service-sandbox.d.ts +4 -0
- package/dist/agent-service-sandbox.js +74 -19
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mcp/course-skills-relay.d.ts +2 -0
- package/dist/mcp/course-skills-relay.js +54 -0
- package/dist/mcp/course-skills-server.d.ts +49 -0
- package/dist/mcp/course-skills-server.js +439 -0
- package/dist/run-dispatcher.d.ts +2 -0
- package/dist/run-dispatcher.js +61 -2
- package/dist/runtime-capabilities.d.ts +1 -0
- package/dist/runtime-capabilities.js +4 -0
- package/dist/runtime-skills.d.ts +93 -0
- package/dist/runtime-skills.js +401 -0
- package/dist/runtimes/deepseek-tui.d.ts +2 -1
- package/dist/runtimes/deepseek-tui.js +252 -25
- package/dist/runtimes/engine.d.ts +6 -0
- package/dist/runtimes/engine.js +6 -1
- package/dist/runtimes/progress.d.ts +2 -0
- package/dist/runtimes/progress.js +34 -0
- package/dist/tool-observation.d.ts +21 -0
- package/dist/tool-observation.js +120 -0
- package/dist/types.d.ts +26 -2
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/dist/run-dispatcher.d.ts
CHANGED
|
@@ -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;
|
|
@@ -17,6 +18,7 @@ export interface PreparedPersistentTurn {
|
|
|
17
18
|
contextRevision: number;
|
|
18
19
|
runtimeEnv?: NodeJS.ProcessEnv;
|
|
19
20
|
inputAttachmentGrant?: InputAttachmentGrant;
|
|
21
|
+
skillProvider?: PreparedRuntimeSkillProvider;
|
|
20
22
|
}
|
|
21
23
|
export interface PersistentSessionExecution {
|
|
22
24
|
prepareTurn(payload: RunStartPayload): PreparedPersistentTurn;
|
package/dist/run-dispatcher.js
CHANGED
|
@@ -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
|
|
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: {
|
|
@@ -693,6 +749,9 @@ export class RunDispatcher {
|
|
|
693
749
|
...(persistentTurn.runtimeEnv
|
|
694
750
|
? { runtimeEnv: persistentTurn.runtimeEnv }
|
|
695
751
|
: {}),
|
|
752
|
+
...(persistentTurn.skillProvider
|
|
753
|
+
? { skillProvider: persistentTurn.skillProvider }
|
|
754
|
+
: {}),
|
|
696
755
|
}
|
|
697
756
|
: {}),
|
|
698
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()}`);
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export declare const COURSE_SKILLS_BINDING_SCHEMA: "course-skills-binding/0.1";
|
|
2
|
+
export declare const COURSE_SKILLS_PROTOCOL: "course-skills/0.1";
|
|
3
|
+
export declare const COURSE_SKILLS_CALL_SCHEMA: "course-skills-call/0.1";
|
|
4
|
+
export interface RuntimeSkillGrant {
|
|
5
|
+
assetVersionId: string;
|
|
6
|
+
ref: string;
|
|
7
|
+
digest: string;
|
|
8
|
+
}
|
|
9
|
+
export interface RuntimeSkillProviderBinding {
|
|
10
|
+
schemaVersion: typeof COURSE_SKILLS_BINDING_SCHEMA;
|
|
11
|
+
protocol: typeof COURSE_SKILLS_PROTOCOL;
|
|
12
|
+
endpoint: string;
|
|
13
|
+
bearerToken: string;
|
|
14
|
+
expiresAt: string;
|
|
15
|
+
effectiveSkillGrants: RuntimeSkillGrant[];
|
|
16
|
+
}
|
|
17
|
+
export interface RuntimeSkillCatalogEntry extends RuntimeSkillGrant {
|
|
18
|
+
name: string;
|
|
19
|
+
description: string;
|
|
20
|
+
version: string;
|
|
21
|
+
requiredCapabilities: string[];
|
|
22
|
+
}
|
|
23
|
+
export interface RuntimeLoadedSkill {
|
|
24
|
+
ref: string;
|
|
25
|
+
digest: string;
|
|
26
|
+
content: string;
|
|
27
|
+
bytes: number;
|
|
28
|
+
truncated: boolean;
|
|
29
|
+
}
|
|
30
|
+
export interface RuntimeSkillReference extends RuntimeLoadedSkill {
|
|
31
|
+
path: string;
|
|
32
|
+
}
|
|
33
|
+
export interface RuntimeSkillProvider {
|
|
34
|
+
listEligibleSkills(): Promise<RuntimeSkillCatalogEntry[]>;
|
|
35
|
+
loadSkill(ref: string): Promise<RuntimeLoadedSkill>;
|
|
36
|
+
loadReference(ref: string, referencePath: string): Promise<RuntimeSkillReference>;
|
|
37
|
+
}
|
|
38
|
+
export interface PreparedRuntimeSkillProvider {
|
|
39
|
+
binding: RuntimeSkillProviderBinding;
|
|
40
|
+
provider: RuntimeSkillProvider;
|
|
41
|
+
catalog: RuntimeSkillCatalogEntry[];
|
|
42
|
+
}
|
|
43
|
+
export type RuntimeSkillEventState = "applied" | "selected" | "loaded" | "reference_loaded" | "load_failed";
|
|
44
|
+
export interface RuntimeSkillEvent {
|
|
45
|
+
schema_version: "agent-skill-event/0.1";
|
|
46
|
+
kind: "skill_event";
|
|
47
|
+
state: RuntimeSkillEventState;
|
|
48
|
+
ref: string;
|
|
49
|
+
digest: string;
|
|
50
|
+
load_mode: "mcp";
|
|
51
|
+
bytes?: number;
|
|
52
|
+
truncated?: boolean;
|
|
53
|
+
code?: string;
|
|
54
|
+
}
|
|
55
|
+
export declare class RuntimeSkillProviderError extends Error {
|
|
56
|
+
readonly code: string;
|
|
57
|
+
constructor(code: string, message: string);
|
|
58
|
+
}
|
|
59
|
+
export type RuntimeSkillProviderFactory = (rawBinding: unknown) => Promise<PreparedRuntimeSkillProvider>;
|
|
60
|
+
interface RuntimeSkillBindingParseOptions {
|
|
61
|
+
allowInsecureLoopbackForTests?: boolean;
|
|
62
|
+
}
|
|
63
|
+
export declare function parseRuntimeSkillProviderBinding(raw: unknown, now?: number, options?: RuntimeSkillBindingParseOptions): RuntimeSkillProviderBinding;
|
|
64
|
+
/**
|
|
65
|
+
* Parse only the stable grant identity of a replayed binding.
|
|
66
|
+
*
|
|
67
|
+
* Credential values remain the first accepted activation snapshot, so replay validation
|
|
68
|
+
* deliberately checks their shape but not their endpoint/expiry semantics.
|
|
69
|
+
*/
|
|
70
|
+
export declare function parseRuntimeSkillProviderGrantSet(raw: unknown): RuntimeSkillGrant[];
|
|
71
|
+
export declare function prepareRuntimeSkillProvider(rawBinding: unknown, options?: {
|
|
72
|
+
fetchFn?: typeof fetch;
|
|
73
|
+
now?: () => number;
|
|
74
|
+
/** Tests only: production bindings must use HTTPS. */
|
|
75
|
+
allowInsecureLoopbackForTests?: boolean;
|
|
76
|
+
}): Promise<PreparedRuntimeSkillProvider>;
|
|
77
|
+
export declare class HttpRuntimeSkillProvider implements RuntimeSkillProvider {
|
|
78
|
+
private readonly binding;
|
|
79
|
+
private readonly fetchFn;
|
|
80
|
+
private readonly now;
|
|
81
|
+
private readonly grantsByRef;
|
|
82
|
+
constructor(binding: RuntimeSkillProviderBinding, options?: {
|
|
83
|
+
fetchFn?: typeof fetch;
|
|
84
|
+
now?: () => number;
|
|
85
|
+
});
|
|
86
|
+
listEligibleSkills(): Promise<RuntimeSkillCatalogEntry[]>;
|
|
87
|
+
loadSkill(ref: string): Promise<RuntimeLoadedSkill>;
|
|
88
|
+
loadReference(ref: string, referencePath: string): Promise<RuntimeSkillReference>;
|
|
89
|
+
private requireEligible;
|
|
90
|
+
private call;
|
|
91
|
+
}
|
|
92
|
+
export declare function safeReferencePath(value: unknown): string;
|
|
93
|
+
export {};
|