@botlearn-course/daemon 0.0.1 → 0.0.2
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 -1
- package/dist/cli.d.ts +1 -2
- package/dist/cli.js +2 -11
- package/dist/course-client.js +4 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/mcp/report-progress-server.d.ts +21 -0
- package/dist/mcp/report-progress-server.js +136 -0
- package/dist/mcp/report-progress.d.ts +29 -0
- package/dist/mcp/report-progress.js +60 -0
- package/dist/run-dispatcher.js +84 -0
- package/dist/runtime-env.d.ts +4 -0
- package/dist/runtime-env.js +17 -0
- package/dist/runtimes/deepseek-tui.d.ts +6 -2
- package/dist/runtimes/deepseek-tui.js +232 -36
- package/dist/runtimes/engine.d.ts +13 -3
- package/dist/runtimes/engine.js +29 -4
- package/dist/runtimes/progress.d.ts +50 -0
- package/dist/runtimes/progress.js +339 -0
- package/dist/transcript.js +6 -0
- package/dist/types.d.ts +20 -2
- package/package.json +1 -1
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { existsSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { homedir, tmpdir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { MAX_PROGRESS_EVENTS_PER_ATTEMPT, tryNormalizeProgressReport, } from "../mcp/report-progress.js";
|
|
6
|
+
export const DEEPSEEK_PROGRESS_TOOL_ALIASES = new Set([
|
|
7
|
+
"report_progress",
|
|
8
|
+
"mcp_botlearn_report_progress",
|
|
9
|
+
"mcp__botlearn__report_progress",
|
|
10
|
+
]);
|
|
11
|
+
export const DEEPSEEK_PROGRESS_SYSTEM_INSTRUCTION = [
|
|
12
|
+
"BotLearn execution progress reporting:",
|
|
13
|
+
"- Use report_progress only when a meaningful user-visible execution phase starts or completes.",
|
|
14
|
+
"- Use status in_progress at phase start and completed only when that execution phase actually ends.",
|
|
15
|
+
"- Short tasks need no progress report; do not report every command, file read, or retry.",
|
|
16
|
+
"- Keep summary concise and user-facing. Never include hidden reasoning, chain of thought, secrets, tokens, prompts, raw command output, or large code excerpts.",
|
|
17
|
+
"- completed means only that an execution phase ended. It does not complete a course task, checkpoint, learning objective, or human review.",
|
|
18
|
+
"- If report_progress fails, continue the main task and do not bypass it through another tool.",
|
|
19
|
+
].join("\n");
|
|
20
|
+
export class ProgressMcpConfigError extends Error {
|
|
21
|
+
constructor(message) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "ProgressMcpConfigError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function createDeepseekProgressState() {
|
|
27
|
+
return {
|
|
28
|
+
emitted: 0,
|
|
29
|
+
lastProgressKey: null,
|
|
30
|
+
callIds: new Set(),
|
|
31
|
+
limitReported: false,
|
|
32
|
+
invalid: 0,
|
|
33
|
+
duplicate: 0,
|
|
34
|
+
overLimit: 0,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** Return only adapter-side drops; emitted blocks are counted by RunDispatcher after reporting. */
|
|
38
|
+
export function deepseekProgressDispositions(state) {
|
|
39
|
+
if (state.invalid + state.duplicate + state.overLimit === 0)
|
|
40
|
+
return undefined;
|
|
41
|
+
return {
|
|
42
|
+
invalid: state.invalid,
|
|
43
|
+
deduplicated: state.duplicate,
|
|
44
|
+
over_limit: state.overLimit,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function progressSystemContext(systemContext) {
|
|
48
|
+
const context = systemContext?.trim();
|
|
49
|
+
return context
|
|
50
|
+
? `${context}\n\n${DEEPSEEK_PROGRESS_SYSTEM_INSTRUCTION}`
|
|
51
|
+
: DEEPSEEK_PROGRESS_SYSTEM_INSTRUCTION;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Recognize a progress tool start and emit only a typed, whitelisted block. Invalid,
|
|
55
|
+
* duplicate, and over-budget calls remain handled so no generic tool card leaks arguments.
|
|
56
|
+
*/
|
|
57
|
+
export function adaptDeepseekProgressStarted(payload, seq, state) {
|
|
58
|
+
const tool = extractTool(payload);
|
|
59
|
+
if (!tool.name || !DEEPSEEK_PROGRESS_TOOL_ALIASES.has(tool.name)) {
|
|
60
|
+
return { matched: false };
|
|
61
|
+
}
|
|
62
|
+
for (const id of tool.ids)
|
|
63
|
+
state.callIds.add(id);
|
|
64
|
+
const progress = tryNormalizeProgressReport(tool.arguments);
|
|
65
|
+
if (!progress) {
|
|
66
|
+
state.invalid += 1;
|
|
67
|
+
return { matched: true };
|
|
68
|
+
}
|
|
69
|
+
const key = progressKey(progress);
|
|
70
|
+
if (state.lastProgressKey === key) {
|
|
71
|
+
state.duplicate += 1;
|
|
72
|
+
return { matched: true };
|
|
73
|
+
}
|
|
74
|
+
if (state.emitted >= MAX_PROGRESS_EVENTS_PER_ATTEMPT) {
|
|
75
|
+
state.overLimit += 1;
|
|
76
|
+
if (!state.limitReported) {
|
|
77
|
+
state.limitReported = true;
|
|
78
|
+
return { matched: true, limitExceeded: true };
|
|
79
|
+
}
|
|
80
|
+
return { matched: true };
|
|
81
|
+
}
|
|
82
|
+
state.lastProgressKey = key;
|
|
83
|
+
state.emitted += 1;
|
|
84
|
+
return { matched: true, block: { kind: "progress", seq, progress } };
|
|
85
|
+
}
|
|
86
|
+
/** Suppress the result for a recognized progress call, including result envelopes with only ids. */
|
|
87
|
+
export function isDeepseekProgressCompletion(payload, state) {
|
|
88
|
+
const tool = extractTool(payload);
|
|
89
|
+
const namedProgress = Boolean(tool.name && DEEPSEEK_PROGRESS_TOOL_ALIASES.has(tool.name));
|
|
90
|
+
const matchingIds = tool.ids.filter((id) => state.callIds.has(id));
|
|
91
|
+
for (const id of matchingIds)
|
|
92
|
+
state.callIds.delete(id);
|
|
93
|
+
return namedProgress || matchingIds.length > 0;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* DeepSeek MCP auto-injection uses a stdio server launched through `env -i`, so it cannot
|
|
97
|
+
* inherit Course or model credentials from the DeepSeek process.
|
|
98
|
+
*/
|
|
99
|
+
export function progressMcpAutoInjectionSupported(platform = process.platform) {
|
|
100
|
+
// DeepSeek overlays config.env on an inherited process environment. Windows has no
|
|
101
|
+
// env -i equivalent in the fixed runtime, so fail closed instead of handing provider
|
|
102
|
+
// credentials to the progress MCP process.
|
|
103
|
+
return platform !== "win32";
|
|
104
|
+
}
|
|
105
|
+
export function resolveExistingDeepseekMcpConfig(env = process.env, home = homedir()) {
|
|
106
|
+
const explicit = env.DEEPSEEK_MCP_CONFIG?.trim();
|
|
107
|
+
if (explicit)
|
|
108
|
+
return resolveMcpPath(explicit, home);
|
|
109
|
+
const defaultConfigPath = path.join(home, ".deepseek", "config.toml");
|
|
110
|
+
const explicitConfigPath = env.DEEPSEEK_CONFIG_PATH?.trim()
|
|
111
|
+
? resolveMcpPath(env.DEEPSEEK_CONFIG_PATH.trim(), home)
|
|
112
|
+
: null;
|
|
113
|
+
const configPath = explicitConfigPath
|
|
114
|
+
&& (existsSync(explicitConfigPath) || !existsSync(defaultConfigPath))
|
|
115
|
+
? explicitConfigPath
|
|
116
|
+
: defaultConfigPath;
|
|
117
|
+
const configured = readConfiguredMcpPath(configPath, home, env.DEEPSEEK_PROFILE?.trim());
|
|
118
|
+
if (configured)
|
|
119
|
+
return configured;
|
|
120
|
+
const defaultPath = path.join(home, ".deepseek", "mcp.json");
|
|
121
|
+
return existsSync(defaultPath) ? defaultPath : null;
|
|
122
|
+
}
|
|
123
|
+
/** Create an ephemeral config that preserves a user's existing MCP servers and settings. */
|
|
124
|
+
export function createProgressMcpConfig(options = {}) {
|
|
125
|
+
const platform = options.platform ?? process.platform;
|
|
126
|
+
if (!progressMcpAutoInjectionSupported(platform)) {
|
|
127
|
+
throw new ProgressMcpConfigError("report_progress MCP auto-injection is unavailable on Windows because clean child env isolation cannot be guaranteed");
|
|
128
|
+
}
|
|
129
|
+
const serverPath = fileURLToPath(new URL("../mcp/report-progress-server.js", import.meta.url));
|
|
130
|
+
const baseConfigPath = options.baseConfigPath === undefined
|
|
131
|
+
? resolveExistingDeepseekMcpConfig()
|
|
132
|
+
: options.baseConfigPath;
|
|
133
|
+
const baseConfig = loadBaseMcpConfig(baseConfigPath);
|
|
134
|
+
const baseServers = mergeMcpServerFields(baseConfig);
|
|
135
|
+
if (Object.hasOwn(baseServers, "botlearn")) {
|
|
136
|
+
throw new ProgressMcpConfigError("DeepSeek MCP server key 'botlearn' is reserved for BotLearn progress reporting");
|
|
137
|
+
}
|
|
138
|
+
const dir = mkdtempSync(path.join(tmpdir(), "botlearn-progress-mcp-"));
|
|
139
|
+
const configPath = path.join(dir, "mcp.json");
|
|
140
|
+
const stagingPath = path.join(dir, ".mcp.json.tmp");
|
|
141
|
+
const minimalPath = "/usr/bin:/bin";
|
|
142
|
+
const baseSettings = { ...baseConfig };
|
|
143
|
+
delete baseSettings.servers;
|
|
144
|
+
delete baseSettings.mcpServers;
|
|
145
|
+
const config = {
|
|
146
|
+
...baseSettings,
|
|
147
|
+
servers: {
|
|
148
|
+
...baseServers,
|
|
149
|
+
botlearn: {
|
|
150
|
+
command: "/usr/bin/env",
|
|
151
|
+
args: ["-i", `PATH=${minimalPath}`, process.execPath, serverPath],
|
|
152
|
+
env: {},
|
|
153
|
+
disabled: false,
|
|
154
|
+
enabled: true,
|
|
155
|
+
required: false,
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
try {
|
|
160
|
+
writeFileSync(stagingPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
161
|
+
renameSync(stagingPath, configPath);
|
|
162
|
+
return { dir, path: configPath };
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
rmSync(dir, { recursive: true, force: true });
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function loadBaseMcpConfig(configPath) {
|
|
170
|
+
if (!configPath)
|
|
171
|
+
return {};
|
|
172
|
+
try {
|
|
173
|
+
const parsed = JSON.parse(readFileSync(configPath, "utf8"));
|
|
174
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
175
|
+
throw new Error("root must be a JSON object");
|
|
176
|
+
}
|
|
177
|
+
const record = parsed;
|
|
178
|
+
if (record.servers !== undefined
|
|
179
|
+
&& (!record.servers || typeof record.servers !== "object" || Array.isArray(record.servers))) {
|
|
180
|
+
throw new Error("servers must be a JSON object");
|
|
181
|
+
}
|
|
182
|
+
if (record.mcpServers !== undefined
|
|
183
|
+
&& (!record.mcpServers
|
|
184
|
+
|| typeof record.mcpServers !== "object"
|
|
185
|
+
|| Array.isArray(record.mcpServers))) {
|
|
186
|
+
throw new Error("mcpServers must be a JSON object");
|
|
187
|
+
}
|
|
188
|
+
return record;
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
192
|
+
throw new ProgressMcpConfigError(`Cannot preserve existing DeepSeek MCP config ${configPath}: ${message}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function mergeMcpServerFields(config) {
|
|
196
|
+
const servers = objectField(config, "servers") ?? {};
|
|
197
|
+
const compatibleServers = objectField(config, "mcpServers") ?? {};
|
|
198
|
+
const duplicate = Object.keys(servers).find((name) => Object.hasOwn(compatibleServers, name));
|
|
199
|
+
if (duplicate) {
|
|
200
|
+
throw new ProgressMcpConfigError(`DeepSeek MCP server '${duplicate}' is defined in both servers and mcpServers`);
|
|
201
|
+
}
|
|
202
|
+
return { ...compatibleServers, ...servers };
|
|
203
|
+
}
|
|
204
|
+
function readConfiguredMcpPath(configPath, home, selectedProfile) {
|
|
205
|
+
if (!existsSync(configPath))
|
|
206
|
+
return null;
|
|
207
|
+
let raw;
|
|
208
|
+
try {
|
|
209
|
+
raw = readFileSync(configPath, "utf8");
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
213
|
+
throw new ProgressMcpConfigError(`Cannot read DeepSeek config ${configPath}: ${message}`);
|
|
214
|
+
}
|
|
215
|
+
let section = "root";
|
|
216
|
+
let rootValue = null;
|
|
217
|
+
let profileValue = null;
|
|
218
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
219
|
+
const trimmed = line.trim();
|
|
220
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
221
|
+
continue;
|
|
222
|
+
const table = /^\[\s*([^\]]+?)\s*\]\s*(?:#.*)?$/.exec(trimmed);
|
|
223
|
+
if (table) {
|
|
224
|
+
const profile = parseProfileTableName(table[1] ?? "", configPath);
|
|
225
|
+
section = selectedProfile && profile === selectedProfile ? "selected_profile" : "other";
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (section === "other")
|
|
229
|
+
continue;
|
|
230
|
+
const match = /^mcp_config_path\s*=\s*(.*?)\s*$/.exec(trimmed);
|
|
231
|
+
if (!match)
|
|
232
|
+
continue;
|
|
233
|
+
const value = parseTomlPathValue(match[1] ?? "", configPath);
|
|
234
|
+
const resolved = value ? resolveMcpPath(value, home) : null;
|
|
235
|
+
if (section === "selected_profile")
|
|
236
|
+
profileValue = resolved;
|
|
237
|
+
else
|
|
238
|
+
rootValue = resolved;
|
|
239
|
+
}
|
|
240
|
+
return profileValue ?? rootValue;
|
|
241
|
+
}
|
|
242
|
+
function parseProfileTableName(raw, configPath) {
|
|
243
|
+
const bare = /^profiles\s*\.\s*([A-Za-z0-9_-]+)$/.exec(raw);
|
|
244
|
+
if (bare)
|
|
245
|
+
return bare[1] ?? null;
|
|
246
|
+
const doubleQuoted = /^profiles\s*\.\s*("(?:\\.|[^"\\])*")$/.exec(raw);
|
|
247
|
+
if (doubleQuoted) {
|
|
248
|
+
try {
|
|
249
|
+
return JSON.parse(doubleQuoted[1]);
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
253
|
+
throw new ProgressMcpConfigError(`Invalid profile table in ${configPath}: ${message}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const singleQuoted = /^profiles\s*\.\s*'([^']*)'$/.exec(raw);
|
|
257
|
+
return singleQuoted?.[1] ?? null;
|
|
258
|
+
}
|
|
259
|
+
function parseTomlPathValue(raw, configPath) {
|
|
260
|
+
const doubleQuoted = /^("(?:\\.|[^"\\])*")\s*(?:#.*)?$/.exec(raw);
|
|
261
|
+
if (doubleQuoted) {
|
|
262
|
+
try {
|
|
263
|
+
return JSON.parse(doubleQuoted[1]);
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
267
|
+
throw new ProgressMcpConfigError(`Invalid mcp_config_path in ${configPath}: ${message}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const singleQuoted = /^'([^']*)'\s*(?:#.*)?$/.exec(raw);
|
|
271
|
+
if (singleQuoted)
|
|
272
|
+
return singleQuoted[1] ?? "";
|
|
273
|
+
return raw.replace(/\s+#.*$/, "").trim();
|
|
274
|
+
}
|
|
275
|
+
function resolveMcpPath(value, home) {
|
|
276
|
+
if (value === "~")
|
|
277
|
+
return home;
|
|
278
|
+
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
279
|
+
return path.join(home, value.slice(2));
|
|
280
|
+
}
|
|
281
|
+
return path.resolve(value);
|
|
282
|
+
}
|
|
283
|
+
export function cleanupProgressMcpConfig(config) {
|
|
284
|
+
if (config)
|
|
285
|
+
rmSync(config.dir, { recursive: true, force: true });
|
|
286
|
+
}
|
|
287
|
+
function progressKey(progress) {
|
|
288
|
+
return `${progress.status}\0${progress.summary}`;
|
|
289
|
+
}
|
|
290
|
+
function extractTool(payload) {
|
|
291
|
+
if (!payload || typeof payload !== "object")
|
|
292
|
+
return { ids: [] };
|
|
293
|
+
const root = payload;
|
|
294
|
+
const embedded = objectField(root, "payload");
|
|
295
|
+
const candidates = [
|
|
296
|
+
root,
|
|
297
|
+
objectField(root, "tool"),
|
|
298
|
+
embedded,
|
|
299
|
+
objectField(embedded, "tool"),
|
|
300
|
+
].filter((candidate) => candidate !== undefined);
|
|
301
|
+
let name;
|
|
302
|
+
let args;
|
|
303
|
+
for (const candidate of candidates) {
|
|
304
|
+
name ??= stringField(candidate, "name") ?? stringField(candidate, "tool_name");
|
|
305
|
+
if (args === undefined) {
|
|
306
|
+
if (Object.hasOwn(candidate, "input"))
|
|
307
|
+
args = candidate.input;
|
|
308
|
+
else if (Object.hasOwn(candidate, "arguments"))
|
|
309
|
+
args = candidate.arguments;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const ids = new Set();
|
|
313
|
+
for (const candidate of candidates) {
|
|
314
|
+
for (const key of ["id", "call_id", "tool_call_id", "item_id"]) {
|
|
315
|
+
const value = stringField(candidate, key);
|
|
316
|
+
if (value)
|
|
317
|
+
ids.add(value);
|
|
318
|
+
}
|
|
319
|
+
const item = objectField(candidate, "item");
|
|
320
|
+
const itemId = stringField(item, "id");
|
|
321
|
+
if (itemId)
|
|
322
|
+
ids.add(itemId);
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
...(name ? { name } : {}),
|
|
326
|
+
...(args !== undefined ? { arguments: args } : {}),
|
|
327
|
+
ids: [...ids],
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function objectField(value, key) {
|
|
331
|
+
const field = value?.[key];
|
|
332
|
+
return field && typeof field === "object" && !Array.isArray(field)
|
|
333
|
+
? field
|
|
334
|
+
: undefined;
|
|
335
|
+
}
|
|
336
|
+
function stringField(value, key) {
|
|
337
|
+
const field = value?.[key];
|
|
338
|
+
return typeof field === "string" ? field : undefined;
|
|
339
|
+
}
|
package/dist/transcript.js
CHANGED
|
@@ -15,6 +15,12 @@ export class TranscriptWriter {
|
|
|
15
15
|
}
|
|
16
16
|
writeBlock(block) {
|
|
17
17
|
const record = { type: "block", kind: block.kind };
|
|
18
|
+
if (block.kind === "progress") {
|
|
19
|
+
record.summary = redactSecretString(block.summary);
|
|
20
|
+
record.status = block.status;
|
|
21
|
+
this.append(record);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
18
24
|
if (block.text !== undefined)
|
|
19
25
|
record.text = redactSecretString(block.text);
|
|
20
26
|
if (block.raw !== undefined)
|
package/dist/types.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* 本包不依赖 BotCord Hub/room/owner-chat 语义;wire 类型与后端
|
|
5
5
|
* `backend/botlearn_course/schemas.py` 的 daemon 契约严格对齐。
|
|
6
6
|
*/
|
|
7
|
+
import type { ProgressStatus } from "./mcp/report-progress.js";
|
|
7
8
|
/** `GET /daemon/runs/next` 下发的 run.start 载荷(snake_case,与后端 RunStartPayloadOut 一致)。 */
|
|
8
9
|
export interface RunStartPayload {
|
|
9
10
|
agent_run_id: string;
|
|
@@ -106,12 +107,27 @@ export interface AppliedRunRuntimeProfile {
|
|
|
106
107
|
skillsRoot: string;
|
|
107
108
|
skillRefs: string[];
|
|
108
109
|
}
|
|
109
|
-
/** runtime adapter
|
|
110
|
-
export interface
|
|
110
|
+
/** runtime adapter 产出的普通归一化块;raw 仅进本地 transcript。 */
|
|
111
|
+
export interface RuntimeContentBlock {
|
|
111
112
|
kind: "text_delta" | "text" | "thinking" | "tool_call" | "tool_result" | "status" | "error";
|
|
112
113
|
text?: string;
|
|
113
114
|
raw?: unknown;
|
|
114
115
|
}
|
|
116
|
+
/** 已由 provider adapter 严格归一化、无 provider raw envelope 的进度遥测。 */
|
|
117
|
+
export interface RuntimeProgressBlock {
|
|
118
|
+
kind: "progress";
|
|
119
|
+
runtime: string;
|
|
120
|
+
summary: string;
|
|
121
|
+
status: ProgressStatus;
|
|
122
|
+
raw?: never;
|
|
123
|
+
}
|
|
124
|
+
export type RuntimeBlock = RuntimeContentBlock | RuntimeProgressBlock;
|
|
125
|
+
/** runtime adapter 在发出进度块前丢弃的非内容处置计数。 */
|
|
126
|
+
export interface RuntimeProgressDispositions {
|
|
127
|
+
invalid: number;
|
|
128
|
+
deduplicated: number;
|
|
129
|
+
over_limit: number;
|
|
130
|
+
}
|
|
115
131
|
export interface RuntimeAuthProbe {
|
|
116
132
|
checked: boolean;
|
|
117
133
|
ok: boolean;
|
|
@@ -129,6 +145,8 @@ export interface CourseRuntimeSink {
|
|
|
129
145
|
block(block: RuntimeBlock): Promise<void>;
|
|
130
146
|
message(text: string): Promise<void>;
|
|
131
147
|
file(file: RunFileCandidate): Promise<void>;
|
|
148
|
+
/** 可选的 run-scoped 内部遥测;不得包含 summary 或 provider raw envelope。 */
|
|
149
|
+
progressDispositions?(dispositions: RuntimeProgressDispositions): Promise<void>;
|
|
132
150
|
}
|
|
133
151
|
/** 一次 run 的本地执行上下文:服务器 payload + daemon 本地准备产物。 */
|
|
134
152
|
export interface RunExecution {
|
package/package.json
CHANGED