@botlearn-course/daemon 0.0.14 → 0.0.16
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-client.d.ts +2 -2
- package/dist/agent-service-client.js +12 -1
- package/dist/agent-service-sandbox.d.ts +13 -2
- package/dist/agent-service-sandbox.js +206 -8
- package/dist/agent-service-ws-protocol.d.ts +1 -1
- package/dist/agent-service-ws-protocol.js +1 -0
- package/dist/course-client.d.ts +2 -2
- package/dist/course-client.js +12 -1
- package/dist/log.d.ts +9 -0
- package/dist/log.js +24 -2
- package/dist/run-dispatcher.d.ts +2 -2
- package/dist/run-dispatcher.js +123 -31
- package/dist/types.d.ts +5 -0
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CourseRuntimeProfile, RunEvent, RunFileCandidate, RunFileRecord, RunStartPayload } from "./types.js";
|
|
1
|
+
import type { CourseRuntimeProfile, RunEvent, RunEventReceipt, RunFileCandidate, RunFileRecord, RunStartPayload } from "./types.js";
|
|
2
2
|
/** Run-scoped client used only inside an Agent Service sandbox. */
|
|
3
3
|
export declare class AgentServiceRunClient {
|
|
4
4
|
private readonly baseUrl;
|
|
@@ -10,7 +10,7 @@ export declare class AgentServiceRunClient {
|
|
|
10
10
|
private url;
|
|
11
11
|
private request;
|
|
12
12
|
getRun(): Promise<RunStartPayload>;
|
|
13
|
-
postEvent(agentRunId: string, event: RunEvent): Promise<
|
|
13
|
+
postEvent(agentRunId: string, event: RunEvent): Promise<RunEventReceipt>;
|
|
14
14
|
postFile(agentRunId: string, file: RunFileCandidate): Promise<RunFileRecord>;
|
|
15
15
|
getRunRuntimeProfile(agentRunId: string): Promise<CourseRuntimeProfile>;
|
|
16
16
|
heartbeat(): Promise<void>;
|
|
@@ -47,7 +47,18 @@ export class AgentServiceRunClient {
|
|
|
47
47
|
this.assertRunId(agentRunId);
|
|
48
48
|
this.traceId = event.trace_id ?? this.traceId;
|
|
49
49
|
const sanitized = redactSecretsDeep(event, 8, [this.runToken]);
|
|
50
|
-
await this.request("POST", `/course/v1/agent-service/runs/${agentRunId}/events`, sanitized);
|
|
50
|
+
const response = await this.request("POST", `/course/v1/agent-service/runs/${agentRunId}/events`, sanitized);
|
|
51
|
+
return response?.disposition === "retry"
|
|
52
|
+
? {
|
|
53
|
+
disposition: "retry",
|
|
54
|
+
...(typeof response.candidate_attempt === "number"
|
|
55
|
+
? { candidate_attempt: response.candidate_attempt }
|
|
56
|
+
: {}),
|
|
57
|
+
...(typeof response.retry_feedback === "string"
|
|
58
|
+
? { retry_feedback: response.retry_feedback }
|
|
59
|
+
: {}),
|
|
60
|
+
}
|
|
61
|
+
: { disposition: "accepted" };
|
|
51
62
|
}
|
|
52
63
|
async postFile(agentRunId, file) {
|
|
53
64
|
this.assertRunId(agentRunId);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Logger } from "./log.js";
|
|
2
2
|
import { type RuntimeSkillProviderFactory } from "./runtime-skills.js";
|
|
3
3
|
import { type PersistentSessionExecution, type PreparedPersistentTurn, type RunReportingClient } from "./run-dispatcher.js";
|
|
4
|
-
import type { CourseRuntime, CourseRuntimeProfile, RunEvent, RunFileCandidate, RunFileRecord, RunStartPayload } from "./types.js";
|
|
4
|
+
import type { CourseRuntime, CourseRuntimeProfile, RunEvent, RunEventReceipt, RunFileCandidate, RunFileRecord, RunStartPayload } from "./types.js";
|
|
5
5
|
export interface AgentServiceSandboxOptions {
|
|
6
6
|
wsUrl: string;
|
|
7
7
|
sandboxId: string;
|
|
@@ -56,6 +56,15 @@ export declare class AgentServiceSandboxClient implements RunReportingClient, Pe
|
|
|
56
56
|
private stopped;
|
|
57
57
|
private permanentFailure;
|
|
58
58
|
private lifecycleChain;
|
|
59
|
+
private runtimeLogPolicy;
|
|
60
|
+
private runtimeLogHandshakeReady;
|
|
61
|
+
private runtimeLogWindowStartedAt;
|
|
62
|
+
private runtimeLogWindowEvents;
|
|
63
|
+
private runtimeLogWindowBytes;
|
|
64
|
+
private runtimeLogSuppressedEvents;
|
|
65
|
+
private runtimeLogSuppressedBytes;
|
|
66
|
+
private runtimeLogSendChain;
|
|
67
|
+
private readonly removeOperationalLogSink;
|
|
59
68
|
constructor(options: AgentServiceSandboxOptions);
|
|
60
69
|
prepareTurn(payload: RunStartPayload): PreparedPersistentTurn;
|
|
61
70
|
persistNativeSession(nativeSessionId: string): void;
|
|
@@ -64,11 +73,13 @@ export declare class AgentServiceSandboxClient implements RunReportingClient, Pe
|
|
|
64
73
|
private recoverPendingActivationCleanups;
|
|
65
74
|
run(): Promise<void>;
|
|
66
75
|
stop(): void;
|
|
67
|
-
postEvent(agentRunId: string, event: RunEvent): Promise<
|
|
76
|
+
postEvent(agentRunId: string, event: RunEvent): Promise<RunEventReceipt>;
|
|
68
77
|
postFile(agentRunId: string, file: RunFileCandidate): Promise<RunFileRecord>;
|
|
69
78
|
getRunRuntimeProfile(agentRunId: string): Promise<CourseRuntimeProfile>;
|
|
70
79
|
private connectOnce;
|
|
71
80
|
private handleHello;
|
|
81
|
+
private forwardOperationalLog;
|
|
82
|
+
private queueRuntimeLog;
|
|
72
83
|
private assertServerFrame;
|
|
73
84
|
private handleServerFrame;
|
|
74
85
|
/** sandbox.sync 只做对账:重放 spool、关闭待关 session、应用 drain/shutdown。 */
|
|
@@ -2,7 +2,7 @@ import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readF
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { ensureDaemonHome } from "./auth-store.js";
|
|
4
4
|
import { AGENT_SERVICE_WS_SCHEMA, AGENT_SERVICE_WS_SUBPROTOCOL, createSandboxFrame, parseSandboxFrame, UnsupportedSandboxProtocolError, } from "./agent-service-ws-protocol.js";
|
|
5
|
-
import { log as defaultLog } from "./log.js";
|
|
5
|
+
import { log as defaultLog, setOperationalLogSink, } from "./log.js";
|
|
6
6
|
import { availableRunCapabilities, runtimeSupportsCourseSkills, } from "./runtime-capabilities.js";
|
|
7
7
|
import { activationRuntimeEnv, runtimeChildEnv } from "./runtime-env.js";
|
|
8
8
|
import { redactSecretString } from "./redaction.js";
|
|
@@ -17,6 +17,70 @@ const MAX_EVENT_ACK_WINDOW = 64;
|
|
|
17
17
|
const LEGACY_EVENT_ACK_WINDOW = 1;
|
|
18
18
|
/** 出站 seq 基址:seq = connection_epoch * SEQ_EPOCH_BASE + n,跨重连单调(合同 §1.1)。 */
|
|
19
19
|
const SEQ_EPOCH_BASE = 1_000_000_000;
|
|
20
|
+
const RUNTIME_LOG_WINDOW_MS = 60_000;
|
|
21
|
+
const DISABLED_RUNTIME_LOG_POLICY = {
|
|
22
|
+
enabled: false,
|
|
23
|
+
maxEventBytes: 4096,
|
|
24
|
+
maxEventsPerMinute: 1,
|
|
25
|
+
maxBytesPerMinute: 4096,
|
|
26
|
+
};
|
|
27
|
+
function boundedUtf8(value, maxBytes) {
|
|
28
|
+
const encoded = Buffer.from(value, "utf8");
|
|
29
|
+
if (encoded.length <= maxBytes)
|
|
30
|
+
return value;
|
|
31
|
+
const suffix = "…[truncated]";
|
|
32
|
+
const budget = Math.max(0, maxBytes - Buffer.byteLength(suffix, "utf8"));
|
|
33
|
+
return encoded.subarray(0, budget).toString("utf8").replace(/�$/u, "") + suffix;
|
|
34
|
+
}
|
|
35
|
+
function boundedLogFields(fields, maxBytes) {
|
|
36
|
+
if (!fields || maxBytes < 2)
|
|
37
|
+
return { bytes: 0 };
|
|
38
|
+
const bounded = {};
|
|
39
|
+
let truncated = false;
|
|
40
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
41
|
+
bounded[key] = value;
|
|
42
|
+
let bytes;
|
|
43
|
+
try {
|
|
44
|
+
bytes = Buffer.byteLength(JSON.stringify(bounded), "utf8");
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
bytes = maxBytes + 1;
|
|
48
|
+
}
|
|
49
|
+
if (bytes > maxBytes) {
|
|
50
|
+
delete bounded[key];
|
|
51
|
+
truncated = true;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (truncated) {
|
|
56
|
+
const marked = { ...bounded, runtime_log_fields_truncated: true };
|
|
57
|
+
if (Buffer.byteLength(JSON.stringify(marked), "utf8") <= maxBytes) {
|
|
58
|
+
Object.assign(bounded, { runtime_log_fields_truncated: true });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (!Object.keys(bounded).length)
|
|
62
|
+
return { bytes: 0 };
|
|
63
|
+
const bytes = Buffer.byteLength(JSON.stringify(bounded), "utf8");
|
|
64
|
+
return { fields: bounded, bytes };
|
|
65
|
+
}
|
|
66
|
+
function runtimeLogPolicy(value) {
|
|
67
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
68
|
+
return DISABLED_RUNTIME_LOG_POLICY;
|
|
69
|
+
}
|
|
70
|
+
const raw = value;
|
|
71
|
+
const integer = (key, fallback, min, max) => {
|
|
72
|
+
const candidate = Number(raw[key]);
|
|
73
|
+
return Number.isSafeInteger(candidate) && candidate >= min && candidate <= max
|
|
74
|
+
? candidate
|
|
75
|
+
: fallback;
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
enabled: raw.enabled === true,
|
|
79
|
+
maxEventBytes: integer("max_event_bytes", 4096, 256, 16_384),
|
|
80
|
+
maxEventsPerMinute: integer("max_events_per_minute", 60, 1, 600),
|
|
81
|
+
maxBytesPerMinute: integer("max_bytes_per_minute", 131_072, 4096, 1_048_576),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
20
84
|
function inputAttachmentGrant(value) {
|
|
21
85
|
if (value === undefined || value === null)
|
|
22
86
|
return undefined;
|
|
@@ -277,6 +341,15 @@ export class AgentServiceSandboxClient {
|
|
|
277
341
|
stopped = false;
|
|
278
342
|
permanentFailure = false;
|
|
279
343
|
lifecycleChain = Promise.resolve();
|
|
344
|
+
runtimeLogPolicy = DISABLED_RUNTIME_LOG_POLICY;
|
|
345
|
+
runtimeLogHandshakeReady = false;
|
|
346
|
+
runtimeLogWindowStartedAt = 0;
|
|
347
|
+
runtimeLogWindowEvents = 0;
|
|
348
|
+
runtimeLogWindowBytes = 0;
|
|
349
|
+
runtimeLogSuppressedEvents = 0;
|
|
350
|
+
runtimeLogSuppressedBytes = 0;
|
|
351
|
+
runtimeLogSendChain = Promise.resolve();
|
|
352
|
+
removeOperationalLogSink;
|
|
280
353
|
constructor(options) {
|
|
281
354
|
this.options = options;
|
|
282
355
|
this.log = options.log ?? defaultLog;
|
|
@@ -288,6 +361,9 @@ export class AgentServiceSandboxClient {
|
|
|
288
361
|
this.dispatcher = new RunDispatcher(this, options.runtimes, {
|
|
289
362
|
persistentSession: this,
|
|
290
363
|
});
|
|
364
|
+
this.removeOperationalLogSink = setOperationalLogSink((entry) => {
|
|
365
|
+
this.forwardOperationalLog(entry);
|
|
366
|
+
});
|
|
291
367
|
}
|
|
292
368
|
prepareTurn(payload) {
|
|
293
369
|
const scope = this.turnScopes.get(payload.agent_run_id);
|
|
@@ -342,9 +418,8 @@ export class AgentServiceSandboxClient {
|
|
|
342
418
|
const scope = this.turnScopes.get(payload.agent_run_id);
|
|
343
419
|
if (!scope)
|
|
344
420
|
return;
|
|
345
|
-
const activation = this.activationContexts.get(scope.sessionId);
|
|
346
421
|
const session = this.state.sessions[scope.sessionId];
|
|
347
|
-
if (session
|
|
422
|
+
if (session?.activationId === scope.activationId) {
|
|
348
423
|
// Persist the cleanup obligation before attempting chmod. A terminal event has
|
|
349
424
|
// already been durably ACKed at this point, so reconnect/restart must finish this
|
|
350
425
|
// revoke even when the server desired state has already moved to idle.
|
|
@@ -448,6 +523,7 @@ export class AgentServiceSandboxClient {
|
|
|
448
523
|
await this.sleep(delay);
|
|
449
524
|
}
|
|
450
525
|
}
|
|
526
|
+
this.removeOperationalLogSink();
|
|
451
527
|
if (this.permanentFailure) {
|
|
452
528
|
throw new Error("Agent Service sandbox stopped after a permanent protocol failure");
|
|
453
529
|
}
|
|
@@ -464,6 +540,8 @@ export class AgentServiceSandboxClient {
|
|
|
464
540
|
this.activeSessionId = null;
|
|
465
541
|
this.currentTurnSessionId = null;
|
|
466
542
|
this.persist();
|
|
543
|
+
this.runtimeLogHandshakeReady = false;
|
|
544
|
+
this.removeOperationalLogSink();
|
|
467
545
|
this.socket?.close(1000, "daemon_stopping");
|
|
468
546
|
}
|
|
469
547
|
async postEvent(agentRunId, event) {
|
|
@@ -524,8 +602,7 @@ export class AgentServiceSandboxClient {
|
|
|
524
602
|
reject: rejectAck,
|
|
525
603
|
});
|
|
526
604
|
await this.sendFrame(frame);
|
|
527
|
-
|
|
528
|
-
await ack;
|
|
605
|
+
return event.type !== "run.block" ? await ack : { disposition: "accepted" };
|
|
529
606
|
}
|
|
530
607
|
async postFile(agentRunId, file) {
|
|
531
608
|
const scope = this.turnScopes.get(agentRunId);
|
|
@@ -639,6 +716,7 @@ export class AgentServiceSandboxClient {
|
|
|
639
716
|
this.rejectPendingFiles(new Error("Agent Service file transport disconnected"));
|
|
640
717
|
if (this.socket === socket)
|
|
641
718
|
this.socket = null;
|
|
719
|
+
this.runtimeLogHandshakeReady = false;
|
|
642
720
|
if (socket.readyState === WebSocketClient.OPEN)
|
|
643
721
|
socket.close(1000, "reconnecting");
|
|
644
722
|
}
|
|
@@ -683,6 +761,7 @@ export class AgentServiceSandboxClient {
|
|
|
683
761
|
? advertisedAckWindow
|
|
684
762
|
: LEGACY_EVENT_ACK_WINDOW;
|
|
685
763
|
this.maxUnackedEvents = Math.min(MAX_EVENT_ACK_WINDOW, ackWindow);
|
|
764
|
+
this.runtimeLogPolicy = runtimeLogPolicy(frame.payload.runtime_log_policy);
|
|
686
765
|
this.persist();
|
|
687
766
|
await this.sendControlFrame("sandbox.ready", {
|
|
688
767
|
protocol_versions: [AGENT_SERVICE_WS_SCHEMA],
|
|
@@ -696,6 +775,73 @@ export class AgentServiceSandboxClient {
|
|
|
696
775
|
resumed_sessions: Object.keys(this.state.sessions),
|
|
697
776
|
spool_frames: this.spoolFrameCount(),
|
|
698
777
|
});
|
|
778
|
+
this.runtimeLogHandshakeReady = true;
|
|
779
|
+
}
|
|
780
|
+
forwardOperationalLog(entry) {
|
|
781
|
+
const policy = this.runtimeLogPolicy;
|
|
782
|
+
if (!policy.enabled || !this.runtimeLogHandshakeReady)
|
|
783
|
+
return;
|
|
784
|
+
const message = boundedUtf8(entry.message, policy.maxEventBytes);
|
|
785
|
+
const messageBytes = Buffer.byteLength(message, "utf8");
|
|
786
|
+
const boundedFields = boundedLogFields(entry.fields, Math.max(0, policy.maxEventBytes - messageBytes));
|
|
787
|
+
const contentBytes = messageBytes + boundedFields.bytes;
|
|
788
|
+
const now = Date.now();
|
|
789
|
+
if (now - this.runtimeLogWindowStartedAt >= RUNTIME_LOG_WINDOW_MS) {
|
|
790
|
+
if (this.runtimeLogSuppressedEvents > 0) {
|
|
791
|
+
this.queueRuntimeLog({
|
|
792
|
+
level: "warn",
|
|
793
|
+
message: "sandbox runtime logs suppressed at daemon source",
|
|
794
|
+
fields: {
|
|
795
|
+
suppressed_count: this.runtimeLogSuppressedEvents,
|
|
796
|
+
suppressed_bytes: this.runtimeLogSuppressedBytes,
|
|
797
|
+
},
|
|
798
|
+
timestamp: new Date(now).toISOString(),
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
this.runtimeLogWindowStartedAt = now;
|
|
802
|
+
this.runtimeLogWindowEvents = 0;
|
|
803
|
+
this.runtimeLogWindowBytes = 0;
|
|
804
|
+
this.runtimeLogSuppressedEvents = 0;
|
|
805
|
+
this.runtimeLogSuppressedBytes = 0;
|
|
806
|
+
}
|
|
807
|
+
if (this.runtimeLogWindowEvents >= policy.maxEventsPerMinute ||
|
|
808
|
+
this.runtimeLogWindowBytes + contentBytes > policy.maxBytesPerMinute) {
|
|
809
|
+
this.runtimeLogSuppressedEvents += 1;
|
|
810
|
+
this.runtimeLogSuppressedBytes += contentBytes;
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
this.runtimeLogWindowEvents += 1;
|
|
814
|
+
this.runtimeLogWindowBytes += contentBytes;
|
|
815
|
+
this.queueRuntimeLog({
|
|
816
|
+
...entry,
|
|
817
|
+
message,
|
|
818
|
+
fields: boundedFields.fields,
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
queueRuntimeLog(entry) {
|
|
822
|
+
this.runtimeLogSendChain = this.runtimeLogSendChain
|
|
823
|
+
.then(async () => {
|
|
824
|
+
if (!this.runtimeLogHandshakeReady || this.socket?.readyState !== WebSocketClient.OPEN) {
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
await this.sendFrame(createSandboxFrame({
|
|
828
|
+
type: "sandbox.log",
|
|
829
|
+
sandboxId: this.options.sandboxId,
|
|
830
|
+
sandboxGeneration: this.sandboxGeneration,
|
|
831
|
+
connectionEpoch: this.connectionEpoch,
|
|
832
|
+
seq: this.nextOutboundSeq(),
|
|
833
|
+
payload: {
|
|
834
|
+
level: entry.level,
|
|
835
|
+
stream: "stderr",
|
|
836
|
+
message: entry.message,
|
|
837
|
+
emitted_at: entry.timestamp,
|
|
838
|
+
...(entry.fields ? { fields: entry.fields } : {}),
|
|
839
|
+
},
|
|
840
|
+
}));
|
|
841
|
+
})
|
|
842
|
+
.catch(() => {
|
|
843
|
+
// Sentry forwarding is best-effort and must not enter the daemon logger.
|
|
844
|
+
});
|
|
699
845
|
}
|
|
700
846
|
assertServerFrame(frame) {
|
|
701
847
|
if (frame.sandbox_id !== this.options.sandboxId ||
|
|
@@ -1088,12 +1234,54 @@ export class AgentServiceSandboxClient {
|
|
|
1088
1234
|
});
|
|
1089
1235
|
}
|
|
1090
1236
|
async handleTurnCancel(frame) {
|
|
1091
|
-
|
|
1237
|
+
const sessionId = frame.runtime_session_id;
|
|
1238
|
+
const runId = frame.agent_run_id;
|
|
1239
|
+
const workerAttempt = frame.worker_attempt;
|
|
1240
|
+
const activationId = frame.activation_id;
|
|
1241
|
+
const requestedScope = { sessionId, workerAttempt, activationId };
|
|
1242
|
+
const existingScope = this.turnScopes.get(runId);
|
|
1243
|
+
if (existingScope && !this.sameTurnScope(existingScope, requestedScope)) {
|
|
1244
|
+
await this.sendCommandAck(frame, "rejected", "turn_scope_mismatch");
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
const session = this.state.sessions[sessionId];
|
|
1248
|
+
if (!session) {
|
|
1249
|
+
await this.sendCommandAck(frame, "rejected", "session_not_open");
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
const accepted = Object.values(session.acceptedCommands).find((command) => command.agentRunId === runId);
|
|
1253
|
+
if (accepted && (accepted.workerAttempt !== workerAttempt ||
|
|
1254
|
+
accepted.activationId !== activationId)) {
|
|
1092
1255
|
await this.sendCommandAck(frame, "rejected", "turn_scope_mismatch");
|
|
1093
1256
|
return;
|
|
1094
1257
|
}
|
|
1095
|
-
this.
|
|
1258
|
+
this.turnScopes.set(runId, requestedScope);
|
|
1259
|
+
if (this.dispatcher.cancel(runId)) {
|
|
1260
|
+
await this.sendCommandAck(frame, "ok");
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
// ``turn.cancel`` may win the race before ``turn.start`` is delivered, or arrive
|
|
1264
|
+
// after a daemon restart lost the runtime process. The fenced server command is
|
|
1265
|
+
// authoritative: ACK it and synthesize the durable terminal event without touching
|
|
1266
|
+
// the sandbox generation or deleting the runtime-session workspace.
|
|
1096
1267
|
await this.sendCommandAck(frame, "ok");
|
|
1268
|
+
await this.postEvent(runId, {
|
|
1269
|
+
type: "run.cancelled",
|
|
1270
|
+
event_id: `cancel-${runId}-${workerAttempt}`,
|
|
1271
|
+
seq: 1,
|
|
1272
|
+
payload: {
|
|
1273
|
+
reason: "cancelled_by_request",
|
|
1274
|
+
runtime: session.runtimeId,
|
|
1275
|
+
},
|
|
1276
|
+
});
|
|
1277
|
+
const commandId = `run:${runId}:${workerAttempt}`;
|
|
1278
|
+
if (!session.completedCommands.includes(commandId)) {
|
|
1279
|
+
session.completedCommands.push(commandId);
|
|
1280
|
+
session.completedCommands = session.completedCommands.slice(-256);
|
|
1281
|
+
}
|
|
1282
|
+
delete session.acceptedCommands[commandId];
|
|
1283
|
+
this.persist();
|
|
1284
|
+
this.finishTurn({ agent_run_id: runId });
|
|
1097
1285
|
}
|
|
1098
1286
|
/**
|
|
1099
1287
|
* 幂等关闭一个 runtime session:终止其 runtime 子进程、删除 workspace/native state、
|
|
@@ -1323,7 +1511,17 @@ export class AgentServiceSandboxClient {
|
|
|
1323
1511
|
this.persist();
|
|
1324
1512
|
if (pending) {
|
|
1325
1513
|
this.pendingAcks.delete(frameId);
|
|
1326
|
-
pending.resolve(
|
|
1514
|
+
pending.resolve(frame.payload.disposition === "retry"
|
|
1515
|
+
? {
|
|
1516
|
+
disposition: "retry",
|
|
1517
|
+
...(typeof frame.payload.candidate_attempt === "number"
|
|
1518
|
+
? { candidate_attempt: frame.payload.candidate_attempt }
|
|
1519
|
+
: {}),
|
|
1520
|
+
...(typeof frame.payload.retry_feedback === "string"
|
|
1521
|
+
? { retry_feedback: frame.payload.retry_feedback }
|
|
1522
|
+
: {}),
|
|
1523
|
+
}
|
|
1524
|
+
: { disposition: "accepted" });
|
|
1327
1525
|
}
|
|
1328
1526
|
}
|
|
1329
1527
|
async replaySpool() {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export declare const AGENT_SERVICE_WS_SCHEMA: "botlearn-agent-sandbox-ws/0.2";
|
|
2
2
|
export declare const AGENT_SERVICE_WS_SUBPROTOCOL: "botlearn-agent-sandbox.v2";
|
|
3
|
-
export type SandboxFrameType = "sandbox.hello" | "sandbox.sync" | "session.open" | "session.activate" | "turn.start" | "turn.cancel" | "session.close" | "sandbox.drain" | "sandbox.shutdown" | "event.ack" | "auth.rotate" | "ping" | "sandbox.ready" | "sandbox.heartbeat" | "command.ack" | "session.opened" | "session.closed" | "turn.event" | "turn.file.report" | "sandbox.drained" | "pong" | "protocol.error";
|
|
3
|
+
export type SandboxFrameType = "sandbox.hello" | "sandbox.sync" | "session.open" | "session.activate" | "turn.start" | "turn.cancel" | "session.close" | "sandbox.drain" | "sandbox.shutdown" | "event.ack" | "auth.rotate" | "ping" | "sandbox.ready" | "sandbox.heartbeat" | "command.ack" | "session.opened" | "session.closed" | "turn.event" | "turn.file.report" | "sandbox.drained" | "sandbox.log" | "pong" | "protocol.error";
|
|
4
4
|
export declare class UnsupportedSandboxProtocolError extends Error {
|
|
5
5
|
readonly schemaVersion: unknown;
|
|
6
6
|
constructor(schemaVersion: unknown);
|
package/dist/course-client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CourseRuntimeProfile, DaemonAuth, RunEvent, RunFileCandidate, RunFileRecord, RunStartPayload } from "./types.js";
|
|
1
|
+
import type { CourseRuntimeProfile, DaemonAuth, RunEvent, RunEventReceipt, RunFileCandidate, RunFileRecord, RunStartPayload } from "./types.js";
|
|
2
2
|
export declare class CourseClientError extends Error {
|
|
3
3
|
readonly status: number;
|
|
4
4
|
constructor(status: number, message: string);
|
|
@@ -31,7 +31,7 @@ export declare class CourseClient {
|
|
|
31
31
|
private request;
|
|
32
32
|
/** 领取下一个分配给本 daemon 的 queued run(无则返回 null)。 */
|
|
33
33
|
claimNextRun(): Promise<RunStartPayload | null>;
|
|
34
|
-
postEvent(agentRunId: string, event: RunEvent): Promise<
|
|
34
|
+
postEvent(agentRunId: string, event: RunEvent): Promise<RunEventReceipt>;
|
|
35
35
|
postFile(agentRunId: string, file: RunFileCandidate): Promise<RunFileRecord | void>;
|
|
36
36
|
getRunRuntimeProfile(agentRunId: string): Promise<CourseRuntimeProfile>;
|
|
37
37
|
}
|
package/dist/course-client.js
CHANGED
|
@@ -103,7 +103,18 @@ export class CourseClient {
|
|
|
103
103
|
async postEvent(agentRunId, event) {
|
|
104
104
|
const credentials = [this.accessToken, this.refreshToken].filter((value) => typeof value === "string");
|
|
105
105
|
const sanitized = redactSecretsDeep(event, 8, credentials);
|
|
106
|
-
await this.request("POST", `/course/v1/daemon/runs/${agentRunId}/events`, sanitized, event.trace_id);
|
|
106
|
+
const response = await this.request("POST", `/course/v1/daemon/runs/${agentRunId}/events`, sanitized, event.trace_id);
|
|
107
|
+
return response?.disposition === "retry"
|
|
108
|
+
? {
|
|
109
|
+
disposition: "retry",
|
|
110
|
+
...(typeof response.candidate_attempt === "number"
|
|
111
|
+
? { candidate_attempt: response.candidate_attempt }
|
|
112
|
+
: {}),
|
|
113
|
+
...(typeof response.retry_feedback === "string"
|
|
114
|
+
? { retry_feedback: response.retry_feedback }
|
|
115
|
+
: {}),
|
|
116
|
+
}
|
|
117
|
+
: { disposition: "accepted" };
|
|
107
118
|
}
|
|
108
119
|
async postFile(agentRunId, file) {
|
|
109
120
|
return this.request("POST", `/course/v1/daemon/runs/${agentRunId}/files`, file);
|
package/dist/log.d.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
type Level = "info" | "warn" | "error" | "debug";
|
|
2
|
+
export interface OperationalLogEntry {
|
|
3
|
+
level: Level;
|
|
4
|
+
message: string;
|
|
5
|
+
fields?: Record<string, unknown>;
|
|
6
|
+
timestamp: string;
|
|
7
|
+
}
|
|
8
|
+
export type OperationalLogSink = (entry: OperationalLogEntry) => void;
|
|
9
|
+
/** One managed sandbox daemon runs per process, so it owns one ephemeral remote sink. */
|
|
10
|
+
export declare function setOperationalLogSink(sink: OperationalLogSink): () => void;
|
|
2
11
|
export interface Logger {
|
|
3
12
|
info(msg: string, fields?: Record<string, unknown>): void;
|
|
4
13
|
warn(msg: string, fields?: Record<string, unknown>): void;
|
package/dist/log.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { appendFileSync, mkdirSync, readdirSync, renameSync, statSync, unlinkSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { daemonHome } from "./auth-store.js";
|
|
4
|
-
import { redactSecretsDeep } from "./redaction.js";
|
|
4
|
+
import { redactSecretString, redactSecretsDeep } from "./redaction.js";
|
|
5
5
|
const LOG_DIR = path.join(daemonHome(), "logs");
|
|
6
6
|
const LOG_FILE = path.join(LOG_DIR, "daemon.log");
|
|
7
7
|
const LOG_ROTATE_MAX_BYTES = 10 * 1024 * 1024;
|
|
@@ -18,6 +18,15 @@ function ensureDir() {
|
|
|
18
18
|
}
|
|
19
19
|
inited = true;
|
|
20
20
|
}
|
|
21
|
+
let operationalLogSink = null;
|
|
22
|
+
/** One managed sandbox daemon runs per process, so it owns one ephemeral remote sink. */
|
|
23
|
+
export function setOperationalLogSink(sink) {
|
|
24
|
+
operationalLogSink = sink;
|
|
25
|
+
return () => {
|
|
26
|
+
if (operationalLogSink === sink)
|
|
27
|
+
operationalLogSink = null;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
21
30
|
function formatValue(value) {
|
|
22
31
|
if (value instanceof Error)
|
|
23
32
|
return JSON.stringify(value.stack ?? value.message);
|
|
@@ -131,7 +140,9 @@ function write(level, msg, fields) {
|
|
|
131
140
|
ensureDir();
|
|
132
141
|
// 所有 fields 值序列化前先深度脱敏,token 类值绝不落盘/上屏。
|
|
133
142
|
const safeFields = fields === undefined ? undefined : redactSecretsDeep(fields);
|
|
134
|
-
const
|
|
143
|
+
const safeMessage = redactSecretString(msg);
|
|
144
|
+
const timestamp = new Date();
|
|
145
|
+
const line = formatLogLine(level, safeMessage, safeFields, timestamp);
|
|
135
146
|
try {
|
|
136
147
|
rotateLogIfNeeded(LOG_FILE, Buffer.byteLength(line) + 1);
|
|
137
148
|
appendFileSync(LOG_FILE, line + "\n", { mode: 0o600 });
|
|
@@ -141,6 +152,17 @@ function write(level, msg, fields) {
|
|
|
141
152
|
}
|
|
142
153
|
// 同步镜像到 stderr,前台运行时可直接观察。
|
|
143
154
|
process.stderr.write(line + "\n");
|
|
155
|
+
try {
|
|
156
|
+
operationalLogSink?.({
|
|
157
|
+
level,
|
|
158
|
+
message: safeMessage,
|
|
159
|
+
fields: safeFields,
|
|
160
|
+
timestamp: timestamp.toISOString(),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// Observability must never affect daemon execution or recurse into the logger.
|
|
165
|
+
}
|
|
144
166
|
}
|
|
145
167
|
export const log = {
|
|
146
168
|
info: (msg, fields) => write("info", msg, fields),
|
package/dist/run-dispatcher.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ 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
4
|
import type { PreparedRuntimeSkillProvider } from "./runtime-skills.js";
|
|
5
|
-
import { type CourseRuntimeProfile, type CourseRuntime, type RunEvent, type RunStartPayload } from "./types.js";
|
|
5
|
+
import { type CourseRuntimeProfile, type CourseRuntime, type RunEvent, type RunEventReceipt, type RunStartPayload } from "./types.js";
|
|
6
6
|
export interface RunDispatcherOptions {
|
|
7
7
|
defaultRuntimeId?: string;
|
|
8
8
|
log?: Logger;
|
|
@@ -26,7 +26,7 @@ export interface PersistentSessionExecution {
|
|
|
26
26
|
finishTurn(payload: RunStartPayload): void;
|
|
27
27
|
}
|
|
28
28
|
export interface RunReportingClient {
|
|
29
|
-
postEvent(agentRunId: string, event: RunEvent): Promise<void>;
|
|
29
|
+
postEvent(agentRunId: string, event: RunEvent): Promise<void | RunEventReceipt>;
|
|
30
30
|
postFile(agentRunId: string, file: import("./types.js").RunFileCandidate): Promise<unknown>;
|
|
31
31
|
getRunRuntimeProfile?(agentRunId: string): Promise<CourseRuntimeProfile>;
|
|
32
32
|
}
|
package/dist/run-dispatcher.js
CHANGED
|
@@ -23,6 +23,7 @@ const BLOCK_TEXT_MAX_CHARS = 4000;
|
|
|
23
23
|
const CONTENT_FLUSH_MAX_CHARS = 512;
|
|
24
24
|
const CONTENT_FLUSH_INTERVAL_MS = 100;
|
|
25
25
|
const AGENT_STREAM_SCHEMA_VERSION = "agent-stream/0.1";
|
|
26
|
+
const MAX_CANDIDATE_GENERATION_ATTEMPTS = 3;
|
|
26
27
|
const SAFE_TOOL_NAME = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,79}$/;
|
|
27
28
|
const SAFE_FAILURE_CODE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$/;
|
|
28
29
|
const SAFE_FAILURE_MODEL = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,159}$/;
|
|
@@ -100,6 +101,29 @@ function chunkUnicodeText(value, maxCodePoints) {
|
|
|
100
101
|
}
|
|
101
102
|
return chunks;
|
|
102
103
|
}
|
|
104
|
+
function candidateRetryPayload(payload, feedback) {
|
|
105
|
+
const original = typeof payload.input.text === "string" ? payload.input.text : "";
|
|
106
|
+
const instruction = truncateText(redactSecretString(feedback?.trim() || "Produce a complete learner-facing reply."), 800);
|
|
107
|
+
return {
|
|
108
|
+
...payload,
|
|
109
|
+
input: {
|
|
110
|
+
...payload.input,
|
|
111
|
+
text: [
|
|
112
|
+
"[BotLearn internal response retry]",
|
|
113
|
+
"Replace the previous candidate completely. Do not mention this review or the previous reply.",
|
|
114
|
+
`Correction required: ${instruction}`,
|
|
115
|
+
original ? `Original current request:\n${original}` : "",
|
|
116
|
+
].filter(Boolean).join("\n\n"),
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function replacesLatestConversationTurn(payload) {
|
|
121
|
+
const regeneration = payload.context.regeneration;
|
|
122
|
+
return Boolean(regeneration &&
|
|
123
|
+
typeof regeneration === "object" &&
|
|
124
|
+
!Array.isArray(regeneration) &&
|
|
125
|
+
regeneration.mode === "replace_latest");
|
|
126
|
+
}
|
|
103
127
|
/**
|
|
104
128
|
* Run dispatcher:把 Course Service 下发的 run.start 交给 runtime,
|
|
105
129
|
* 并把 runtime 输出归一化成 run.block / run.message / run.completed 回报 Course Service。
|
|
@@ -265,8 +289,7 @@ export class RunDispatcher {
|
|
|
265
289
|
};
|
|
266
290
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
267
291
|
try {
|
|
268
|
-
await this.client.postEvent(runId, outgoing);
|
|
269
|
-
return;
|
|
292
|
+
return await this.client.postEvent(runId, outgoing);
|
|
270
293
|
}
|
|
271
294
|
catch (err) {
|
|
272
295
|
if (isRunTerminal(err)) {
|
|
@@ -274,7 +297,13 @@ export class RunDispatcher {
|
|
|
274
297
|
controller.abort();
|
|
275
298
|
return;
|
|
276
299
|
}
|
|
277
|
-
const
|
|
300
|
+
const candidateReview = event.type === "run.message"
|
|
301
|
+
&& typeof event.payload?.candidate_attempt === "number";
|
|
302
|
+
// The Course Service already retries the side-effect-free judge once. A known
|
|
303
|
+
// 5xx from that boundary must not multiply one review into six provider calls;
|
|
304
|
+
// transport errors without a response still reuse this event id normally.
|
|
305
|
+
const retryable = !(err instanceof CourseClientError)
|
|
306
|
+
|| (!candidateReview && (err.status === 429 || err.status >= 500));
|
|
278
307
|
if (!retryable || attempt === 2)
|
|
279
308
|
throw err;
|
|
280
309
|
await new Promise((resolve) => setTimeout(resolve, 100 * (attempt + 1)));
|
|
@@ -441,6 +470,14 @@ export class RunDispatcher {
|
|
|
441
470
|
controller.abort();
|
|
442
471
|
}, timeoutSeconds * 1000);
|
|
443
472
|
let finalText = "";
|
|
473
|
+
let activeNativeSessionId = persistentTurn?.nativeSessionId ?? null;
|
|
474
|
+
if (activeNativeSessionId && replacesLatestConversationTurn(payload)) {
|
|
475
|
+
// A user retry is a replacement branch, not a follow-up to the discarded answer.
|
|
476
|
+
// Clear the provider-native cache before execution; the durable Course transcript
|
|
477
|
+
// in this payload is sufficient to rebuild the branch in the same turn.
|
|
478
|
+
activeNativeSessionId = null;
|
|
479
|
+
this.persistentSession?.persistNativeSession("");
|
|
480
|
+
}
|
|
444
481
|
// 上一次真正上了 wire 的块 kind:status 只在 kind 切换时上报一次,避免刷屏。
|
|
445
482
|
let lastReportedKind = null;
|
|
446
483
|
let lastReasoningPhase = null;
|
|
@@ -730,31 +767,93 @@ export class RunDispatcher {
|
|
|
730
767
|
await this.client.postFile(runId, file);
|
|
731
768
|
},
|
|
732
769
|
runtimeSession: async (sessionId) => {
|
|
770
|
+
activeNativeSessionId = sessionId;
|
|
733
771
|
this.persistentSession?.persistNativeSession(sessionId);
|
|
734
772
|
},
|
|
735
773
|
};
|
|
736
774
|
modelStartedAt = this.now();
|
|
775
|
+
let acceptedOutput = "";
|
|
776
|
+
let attemptPayload = payload;
|
|
777
|
+
const resetVisibleCandidate = async () => {
|
|
778
|
+
await send({
|
|
779
|
+
type: "run.block",
|
|
780
|
+
payload: {
|
|
781
|
+
schema_version: AGENT_STREAM_SCHEMA_VERSION,
|
|
782
|
+
kind: "response_reset",
|
|
783
|
+
runtime: runtime.id,
|
|
784
|
+
status: "retrying",
|
|
785
|
+
},
|
|
786
|
+
});
|
|
787
|
+
};
|
|
737
788
|
try {
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
789
|
+
for (let candidateAttempt = 1; candidateAttempt <= MAX_CANDIDATE_GENERATION_ATTEMPTS; candidateAttempt += 1) {
|
|
790
|
+
finalText = "";
|
|
791
|
+
await runtime.run({
|
|
792
|
+
payload: attemptPayload,
|
|
793
|
+
workspaceDir,
|
|
794
|
+
inputAttachments,
|
|
795
|
+
...(persistentTurn
|
|
796
|
+
? {
|
|
797
|
+
...(persistentTurn.runtimeStateDir
|
|
798
|
+
? { runtimeStateDir: persistentTurn.runtimeStateDir }
|
|
799
|
+
: {}),
|
|
800
|
+
nativeSessionId: activeNativeSessionId,
|
|
801
|
+
contextRevision: persistentTurn.contextRevision,
|
|
802
|
+
...(persistentTurn.runtimeEnv
|
|
803
|
+
? { runtimeEnv: persistentTurn.runtimeEnv }
|
|
804
|
+
: {}),
|
|
805
|
+
...(persistentTurn.skillProvider
|
|
806
|
+
? { skillProvider: persistentTurn.skillProvider }
|
|
807
|
+
: {}),
|
|
808
|
+
}
|
|
809
|
+
: {}),
|
|
810
|
+
}, sink, controller.signal);
|
|
811
|
+
await flushContent();
|
|
812
|
+
if (serverTerminal)
|
|
813
|
+
return;
|
|
814
|
+
if (controller.signal.aborted)
|
|
815
|
+
throw new Error("aborted");
|
|
816
|
+
const maxOutputChars = typeof payload.limits.max_output_chars === "number" &&
|
|
817
|
+
Number.isFinite(payload.limits.max_output_chars) &&
|
|
818
|
+
payload.limits.max_output_chars > 0
|
|
819
|
+
? payload.limits.max_output_chars
|
|
820
|
+
: DEFAULT_MAX_OUTPUT_CHARS;
|
|
821
|
+
const output = redactSecretString(truncateText(finalText, maxOutputChars));
|
|
822
|
+
await send({
|
|
823
|
+
type: "run.block",
|
|
824
|
+
payload: {
|
|
825
|
+
schema_version: AGENT_STREAM_SCHEMA_VERSION,
|
|
826
|
+
kind: "response_review",
|
|
827
|
+
runtime: runtime.id,
|
|
828
|
+
status: "in_progress",
|
|
829
|
+
},
|
|
830
|
+
});
|
|
831
|
+
let receipt;
|
|
832
|
+
try {
|
|
833
|
+
receipt = await send({
|
|
834
|
+
type: "run.message",
|
|
835
|
+
role: "assistant",
|
|
836
|
+
text: output,
|
|
837
|
+
payload: { candidate_attempt: candidateAttempt },
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
catch (error) {
|
|
841
|
+
await resetVisibleCandidate();
|
|
842
|
+
throw error;
|
|
843
|
+
}
|
|
844
|
+
if (receipt?.disposition !== "retry") {
|
|
845
|
+
acceptedOutput = output;
|
|
846
|
+
break;
|
|
847
|
+
}
|
|
848
|
+
await resetVisibleCandidate();
|
|
849
|
+
if (candidateAttempt >= MAX_CANDIDATE_GENERATION_ATTEMPTS) {
|
|
850
|
+
await sendFailure("candidate_rejected", "Agent could not produce a complete reply after two retries", new RuntimeExecutionError("Agent candidate rejected after bounded retries"), { candidate_attempts: candidateAttempt });
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
lastReportedKind = null;
|
|
854
|
+
lastReasoningPhase = null;
|
|
855
|
+
attemptPayload = candidateRetryPayload(payload, receipt.retry_feedback);
|
|
856
|
+
}
|
|
758
857
|
}
|
|
759
858
|
finally {
|
|
760
859
|
modelFinishedAt = this.now();
|
|
@@ -764,13 +863,7 @@ export class RunDispatcher {
|
|
|
764
863
|
return;
|
|
765
864
|
if (controller.signal.aborted)
|
|
766
865
|
throw new Error("aborted");
|
|
767
|
-
|
|
768
|
-
Number.isFinite(payload.limits.max_output_chars) &&
|
|
769
|
-
payload.limits.max_output_chars > 0
|
|
770
|
-
? payload.limits.max_output_chars
|
|
771
|
-
: DEFAULT_MAX_OUTPUT_CHARS;
|
|
772
|
-
const output = redactSecretString(truncateText(finalText, maxOutputChars));
|
|
773
|
-
activeTranscript.writeFinal(output);
|
|
866
|
+
activeTranscript.writeFinal(acceptedOutput);
|
|
774
867
|
fileReportStartedAt = this.now();
|
|
775
868
|
try {
|
|
776
869
|
await reportFileCandidates(this.client, runId, workspaceDir, this.log, this.scanLimits);
|
|
@@ -778,7 +871,6 @@ export class RunDispatcher {
|
|
|
778
871
|
finally {
|
|
779
872
|
fileReportFinishedAt = this.now();
|
|
780
873
|
}
|
|
781
|
-
await send({ type: "run.message", role: "assistant", text: output });
|
|
782
874
|
await sendTerminal({
|
|
783
875
|
type: "run.completed",
|
|
784
876
|
payload: { runtime: runtimeId, usage: usage() },
|
package/dist/types.d.ts
CHANGED
|
@@ -68,6 +68,11 @@ export interface RunEvent {
|
|
|
68
68
|
error?: string;
|
|
69
69
|
payload?: Record<string, unknown>;
|
|
70
70
|
}
|
|
71
|
+
export interface RunEventReceipt {
|
|
72
|
+
disposition: "accepted" | "retry";
|
|
73
|
+
candidate_attempt?: number;
|
|
74
|
+
retry_feedback?: string;
|
|
75
|
+
}
|
|
71
76
|
/** `POST /daemon/runs/{id}/files` 的文件候选(与后端 DaemonRunFileIn 一致)。 */
|
|
72
77
|
export interface RunFileCandidate {
|
|
73
78
|
event?: "created" | "modified" | "deleted";
|
package/package.json
CHANGED