@actiondock/core 2.0.2 → 2.0.4

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.
@@ -16,13 +16,14 @@ export interface InitOptions {
16
16
  /**
17
17
  * 在目标目录初始化一个完整的 ActionDock 2.0 Action Package 脚手架。
18
18
  * 生成内容包括:
19
- * 1. actiondock.json(项目元数据与配置声明)
20
- * 2. package.json(模块依赖与 bun test 脚本)
21
- * 3. tsconfig.json(现代 ESNext / Bundler 编译配置)
22
- * 4. .gitignore(排除持久化 db、node_modules、dist)
23
- * 5. actions/greet.ts(标准示例 Action,演示 configstatelog 使用)
24
- * 6. playbooks/greet-user.md(标准 SOP Playbook 演示)
25
- * 7. tests/greet.test.ts(基于 createTestRuntime 的零依赖单元测试)
19
+ * - actiondock.json(项目元数据与配置声明)
20
+ * - actiondock.manifest.json(声明式元数据清单事实源)
21
+ * - package.json(Node.js 标准脚本与依赖声明)
22
+ * - tsconfig.json(现代 NodeNext 模块规范)
23
+ * - .gitignore(排除持久化 dbnode_modulesdist)
24
+ * - actions/greet.ts(标准示例 Action,演示 config、state、log 使用)
25
+ * - playbooks/greet-user.md(标准 SOP Playbook 演示)
26
+ * - tests/greet.test.ts(基于 node:test 与 @actiondock/testing 的测试用例)
26
27
  *
27
28
  * @param targetDir 目标项目目录
28
29
  * @param options 初始化选项
@@ -58,20 +59,61 @@ export function initProject(targetDir: string, options: InitOptions = {}): void
58
59
  JSON.stringify(actiondockJson, null, 2) + "\n"
59
60
  );
60
61
 
61
- // 2. package.json
62
+ // 2. actiondock.manifest.json
63
+ const manifestJson = {
64
+ schemaVersion: 1,
65
+ actions: {
66
+ "sample.greet": {
67
+ entry: "actions/greet.ts",
68
+ description: "Greeting action demonstrating basic input, config, and state usage",
69
+ inputSchema: {
70
+ type: "object",
71
+ properties: {
72
+ name: {
73
+ type: "string",
74
+ description: "Name of the person to greet",
75
+ },
76
+ },
77
+ required: ["name"],
78
+ },
79
+ outputSchema: {
80
+ type: "object",
81
+ properties: {
82
+ message: { type: "string" },
83
+ timesGreeted: { type: "number" },
84
+ },
85
+ required: ["message", "timesGreeted"],
86
+ },
87
+ uses: [],
88
+ tags: ["sample"],
89
+ },
90
+ },
91
+ assets: [],
92
+ };
93
+ writeFileSync(
94
+ join(root, "actiondock.manifest.json"),
95
+ JSON.stringify(manifestJson, null, 2) + "\n"
96
+ );
97
+
98
+ // 3. package.json
62
99
  const packageJson = {
63
100
  name: id,
64
101
  version: "0.1.0",
65
102
  description,
66
103
  type: "module",
67
104
  scripts: {
68
- test: "bun test",
105
+ test: "node --import tsx --test tests/*.test.ts",
106
+ },
107
+ engines: {
108
+ node: ">=22.12.0",
69
109
  },
70
110
  dependencies: {
71
- "@actiondock/sdk": "^2.0.0",
111
+ "@actiondock/sdk": "^2.0.4",
72
112
  },
73
113
  devDependencies: {
74
- "@types/bun": "latest",
114
+ "@actiondock/testing": "^2.0.4",
115
+ "@types/node": "^22.12.0",
116
+ "tsx": "^4.19.0",
75
117
  "typescript": "^5.7.0",
76
118
  },
77
119
  };
@@ -80,15 +122,15 @@ export function initProject(targetDir: string, options: InitOptions = {}): void
80
122
  JSON.stringify(packageJson, null, 2) + "\n"
81
123
  );
82
124
 
83
- // 3. tsconfig.json
125
+ // 4. tsconfig.json
84
126
  const tsconfigJson = {
85
127
  compilerOptions: {
86
- target: "ESNext",
87
- module: "ESNext",
88
- moduleResolution: "bundler",
128
+ target: "ES2022",
129
+ module: "NodeNext",
130
+ moduleResolution: "NodeNext",
89
131
  strict: true,
90
132
  skipLibCheck: true,
91
- types: ["bun-types"],
133
+ types: ["node"],
92
134
  },
93
135
  };
94
136
  writeFileSync(
@@ -96,16 +138,17 @@ export function initProject(targetDir: string, options: InitOptions = {}): void
96
138
  JSON.stringify(tsconfigJson, null, 2) + "\n"
97
139
  );
98
140
 
99
- // 4. .gitignore
141
+ // 5. .gitignore
100
142
  const gitignore = `.actiondock/
101
143
  node_modules/
102
144
  dist/
103
- bun.lock
104
- *.db
145
+ build/
146
+ *.log
147
+ .env
105
148
  `;
106
149
  writeFileSync(join(root, ".gitignore"), gitignore);
107
150
 
108
- // 5. actions/
151
+ // 6. actions/
109
152
  const actionsDir = join(root, "actions");
110
153
  mkdirSync(actionsDir, { recursive: true });
111
154
 
@@ -113,12 +156,15 @@ bun.lock
113
156
 
114
157
  export default defineAction({
115
158
  id: "sample.greet",
116
- description: "Greet a user with configurable greeting",
159
+ description: "Greeting action demonstrating basic input, config, and state usage",
117
160
 
118
161
  inputSchema: {
119
162
  type: "object",
120
163
  properties: {
121
- name: { type: "string", description: "Name of person to greet" },
164
+ name: {
165
+ type: "string",
166
+ description: "Name of the person to greet",
167
+ },
122
168
  },
123
169
  required: ["name"],
124
170
  },
@@ -127,9 +173,9 @@ export default defineAction({
127
173
  type: "object",
128
174
  properties: {
129
175
  message: { type: "string" },
130
- timestamp: { type: "string" },
176
+ timesGreeted: { type: "number" },
131
177
  },
132
- required: ["message", "timestamp"],
178
+ required: ["message", "timesGreeted"],
133
179
  },
134
180
 
135
181
  async run(input: { name: string }, ctx) {
@@ -141,14 +187,14 @@ export default defineAction({
141
187
 
142
188
  return {
143
189
  message: \`\${greeting}, \${input.name}!\`,
144
- timestamp: new Date().toISOString(),
190
+ timesGreeted: count,
145
191
  };
146
192
  },
147
193
  });
148
194
  `;
149
195
  writeFileSync(join(actionsDir, "greet.ts"), sampleAction);
150
196
 
151
- // 6. playbooks/
197
+ // 7. playbooks/
152
198
  const playbooksDir = join(root, "playbooks");
153
199
  mkdirSync(playbooksDir, { recursive: true });
154
200
 
@@ -166,12 +212,13 @@ actions:
166
212
  `;
167
213
  writeFileSync(join(playbooksDir, "greet-user.md"), samplePlaybook);
168
214
 
169
- // 7. tests/
215
+ // 8. tests/
170
216
  const testsDir = join(root, "tests");
171
217
  mkdirSync(testsDir, { recursive: true });
172
218
 
173
- const sampleTest = `import { describe, expect, it } from "bun:test";
174
- import { createTestRuntime } from "@actiondock/sdk";
219
+ const sampleTest = `import { describe, it } from "node:test";
220
+ import assert from "node:assert/strict";
221
+ import { createTestRuntime } from "@actiondock/testing";
175
222
  import greetAction from "../actions/greet";
176
223
 
177
224
  describe("greet action", () => {
@@ -181,12 +228,12 @@ describe("greet action", () => {
181
228
  });
182
229
 
183
230
  const res1 = await runtime.run(greetAction, { name: "Alice" });
184
- expect(res1.message).toBe("Hi, Alice!");
185
- expect(await runtime.state.get("greet_count")).toBe(1);
231
+ assert.equal(res1.message, "Hi, Alice!");
232
+ assert.equal(await runtime.state.get("greet_count"), 1);
186
233
 
187
234
  const res2 = await runtime.run(greetAction, { name: "Bob" });
188
- expect(res2.message).toBe("Hi, Bob!");
189
- expect(await runtime.state.get("greet_count")).toBe(2);
235
+ assert.equal(res2.message, "Hi, Bob!");
236
+ assert.equal(await runtime.state.get("greet_count"), 2);
190
237
  });
191
238
  });
192
239
  `;
@@ -1,3 +1,4 @@
1
+ import { spawnSync } from "node:child_process";
1
2
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
3
  import { basename, dirname, join, resolve } from "node:path";
3
4
  import YAML from "yaml";
@@ -68,29 +69,28 @@ export function loadProjectConfig(projectRoot: string): ProjectConfig {
68
69
  }
69
70
 
70
71
  /**
71
- * 探测宿主系统中可用的包管理工具(优先级:bun > pnpm > yarn > npm)。
72
+ * 探测宿主系统中可用的包管理工具(优先级:pnpm > npm > yarn > bun)。
72
73
  */
73
74
  function getInstallCommand(): string[] {
74
75
  const candidates: [string, string][] = [
75
- ["bun", "install"],
76
76
  ["pnpm", "install"],
77
- ["yarn", "install"],
78
77
  ["npm", "install"],
78
+ ["yarn", "install"],
79
+ ["bun", "install"],
79
80
  ];
80
81
  for (const [pm, action] of candidates) {
81
82
  try {
82
- const check = Bun.spawnSync([pm, "--version"], {
83
- stdout: "pipe",
84
- stderr: "pipe",
83
+ const check = spawnSync(pm, ["--version"], {
84
+ stdio: "pipe",
85
85
  });
86
- if (check.exitCode === 0) {
86
+ if (check.status === 0) {
87
87
  return [pm, action];
88
88
  }
89
89
  } catch {
90
90
  // 继续探测下一个候选包管理器
91
91
  }
92
92
  }
93
- return ["bun", "install"];
93
+ return ["npm", "install"];
94
94
  }
95
95
 
96
96
  /**
@@ -131,13 +131,12 @@ export function ensureProjectDependencies(projectRoot: string, force = false): b
131
131
  `[actiondock] Installing dependencies using ${installCmd[0]} for '${pkg.name || basename(projectRoot)}'...\n`
132
132
  );
133
133
 
134
- const proc = Bun.spawnSync(installCmd, {
134
+ const proc = spawnSync(installCmd[0], installCmd.slice(1), {
135
135
  cwd: projectRoot,
136
- stdout: "pipe",
137
- stderr: "pipe",
136
+ stdio: "pipe",
138
137
  });
139
138
 
140
- if (proc.exitCode !== 0) {
139
+ if (proc.status !== 0) {
141
140
  const errText = proc.stderr?.toString() || `Unknown error during ${installCmd[0]} install`;
142
141
  process.stderr.write(`[actiondock] Warning: Dependency installation failed: ${errText}\n`);
143
142
  return false;
@@ -0,0 +1,96 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import type { ActionDockManifest, ActionManifestEntry } from "./types";
4
+
5
+ export const MANIFEST_FILE_NAME = "actiondock.manifest.json";
6
+
7
+ /**
8
+ * 读取并解析项目的声明式清单文件。
9
+ * 若文件不存在则返回 null。
10
+ */
11
+ export function loadManifest(projectRoot: string): ActionDockManifest | null {
12
+ const filePath = join(projectRoot, MANIFEST_FILE_NAME);
13
+ if (!existsSync(filePath)) {
14
+ return null;
15
+ }
16
+ try {
17
+ const raw = readFileSync(filePath, "utf-8");
18
+ const parsed = JSON.parse(raw) as ActionDockManifest;
19
+ if (!parsed || typeof parsed !== "object" || parsed.schemaVersion !== 1) {
20
+ return null;
21
+ }
22
+ return parsed;
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ /**
29
+ * 保存声明式清单文件至项目根目录。
30
+ */
31
+ export function saveManifest(projectRoot: string, manifest: ActionDockManifest): void {
32
+ const filePath = join(projectRoot, MANIFEST_FILE_NAME);
33
+ writeFileSync(filePath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
34
+ }
35
+
36
+ /**
37
+ * 校验清单数据结构的合法性。
38
+ */
39
+ export function validateManifest(manifest: unknown): { valid: boolean; errors?: string[] } {
40
+ if (!manifest || typeof manifest !== "object") {
41
+ return { valid: false, errors: ["Manifest must be an object"] };
42
+ }
43
+ const m = manifest as ActionDockManifest;
44
+ const errors: string[] = [];
45
+
46
+ if (m.schemaVersion !== 1) {
47
+ errors.push("Manifest 'schemaVersion' must be 1");
48
+ }
49
+ if (!m.actions || typeof m.actions !== "object") {
50
+ errors.push("Manifest 'actions' must be an object");
51
+ } else {
52
+ for (const [actionId, item] of Object.entries(m.actions)) {
53
+ if (!item || typeof item !== "object") {
54
+ errors.push(`Action entry '${actionId}' must be an object`);
55
+ continue;
56
+ }
57
+ if (!item.entry || typeof item.entry !== "string") {
58
+ errors.push(`Action '${actionId}' must specify string 'entry'`);
59
+ }
60
+ if (item.uses && !Array.isArray(item.uses)) {
61
+ errors.push(`Action '${actionId}' property 'uses' must be an array`);
62
+ }
63
+ if (item.tags && !Array.isArray(item.tags)) {
64
+ errors.push(`Action '${actionId}' property 'tags' must be an array`);
65
+ }
66
+ }
67
+ }
68
+
69
+ return {
70
+ valid: errors.length === 0,
71
+ errors: errors.length > 0 ? errors : undefined,
72
+ };
73
+ }
74
+
75
+ /**
76
+ * 为单个 Action 构建清单项。
77
+ */
78
+ export function createManifestEntry(options: {
79
+ entry: string;
80
+ description?: string;
81
+ inputSchema?: Record<string, unknown> | boolean;
82
+ outputSchema?: Record<string, unknown> | boolean;
83
+ uses?: string[];
84
+ tags?: string[];
85
+ annotations?: Record<string, unknown>;
86
+ }): ActionManifestEntry {
87
+ return {
88
+ entry: options.entry,
89
+ description: options.description,
90
+ inputSchema: options.inputSchema,
91
+ outputSchema: options.outputSchema,
92
+ uses: options.uses || [],
93
+ tags: options.tags || [],
94
+ annotations: options.annotations,
95
+ };
96
+ }
@@ -60,3 +60,33 @@ export interface PlaybookDefinition extends PlaybookFrontmatter {
60
60
  /** Playbook 源文件的绝对物理路径 */
61
61
  filePath: string;
62
62
  }
63
+
64
+ /**
65
+ * 单个 Action 在清单中的声明项。
66
+ */
67
+ export interface ActionManifestEntry {
68
+ /** Action 入口文件相对路径(如 "actions/greet.ts") */
69
+ entry: string;
70
+ /** Action 功能描述 */
71
+ description?: string;
72
+ /** 输入参数模式规范 */
73
+ inputSchema?: Record<string, unknown> | boolean;
74
+ /** 输出结果模式规范 */
75
+ outputSchema?: Record<string, unknown> | boolean;
76
+ /** 静态依赖的 Action 列表 */
77
+ uses?: string[];
78
+ /** 标签列表 */
79
+ tags?: string[];
80
+ /** 协议注解元数据 */
81
+ annotations?: Record<string, unknown>;
82
+ }
83
+
84
+ /**
85
+ * ActionDock 声明式清单(actiondock.manifest.json)规范。
86
+ * 作为元数据的单一事实源,实现无副作用的模块发现与构建规划。
87
+ */
88
+ export interface ActionDockManifest {
89
+ schemaVersion: number;
90
+ actions: Record<string, ActionManifestEntry>;
91
+ assets?: string[];
92
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * 统一时间与时钟接口。
3
+ */
4
+ export interface Clock {
5
+ /** 获取当前系统墙上时间 */
6
+ now(): Date;
7
+ /** 获取单调递增时间戳(单位:毫秒) */
8
+ monotonic(): number;
9
+ /** 异步休眠指定毫秒 */
10
+ sleep(ms: number): Promise<void>;
11
+ }
12
+
13
+ /**
14
+ * 生产环境系统时钟实现。
15
+ */
16
+ export class SystemClock implements Clock {
17
+ now(): Date {
18
+ return new Date();
19
+ }
20
+
21
+ monotonic(): number {
22
+ if (typeof performance !== "undefined" && typeof performance.now === "function") {
23
+ return performance.now();
24
+ }
25
+ return Date.now();
26
+ }
27
+
28
+ sleep(ms: number): Promise<void> {
29
+ return new Promise((resolve) => setTimeout(resolve, ms));
30
+ }
31
+ }
32
+
33
+ let defaultClock: Clock = new SystemClock();
34
+
35
+ export function getSystemClock(): Clock {
36
+ return defaultClock;
37
+ }
38
+
39
+ export function setSystemClock(clock: Clock): void {
40
+ defaultClock = clock;
41
+ }
@@ -1,15 +1,19 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import type {
2
3
  ActionContext,
3
4
  ActionDefinition,
4
5
  ActionInvoker,
5
6
  Config,
6
7
  Logger,
8
+ ProcessAPI,
9
+ ProgressReporter,
7
10
  StateStore,
8
11
  } from "@actiondock/sdk";
9
12
  import type { ProjectConfig } from "../project/types";
10
13
  import { createGlobalStorage } from "../storage";
11
14
  import type { RuntimeStorage } from "../storage/types";
12
15
  import { resolveEnvValue } from "./env";
16
+ import { getProcessExecutor } from "./process";
13
17
 
14
18
  /**
15
19
  * 生产级配置解析器实现。
@@ -184,8 +188,12 @@ export interface ContextOptions {
184
188
  overrides?: Record<string, unknown>;
185
189
  projectConfig?: ProjectConfig;
186
190
  parentRunId?: string;
191
+ runId?: string;
192
+ rootRunId?: string;
187
193
  callStack?: string[];
188
194
  signal?: AbortSignal;
195
+ process?: ProcessAPI;
196
+ progress?: ProgressReporter;
189
197
  onActionInvoke?: (
190
198
  action: ActionDefinition,
191
199
  input: unknown,
@@ -196,7 +204,7 @@ export interface ContextOptions {
196
204
  /**
197
205
  * 构建并装配传递给 Action 的完整 ActionContext 运行时上下文。
198
206
  *
199
- * @param options 上下文构建参数(包含存储连接、配置覆盖、项目元数据、取消信号与互调委托)
207
+ * @param options 上下文构建参数
200
208
  * @returns 组装完毕的 ActionContext 实例
201
209
  */
202
210
  export function createActionContext(options: ContextOptions): ActionContext {
@@ -208,6 +216,8 @@ export function createActionContext(options: ContextOptions): ActionContext {
208
216
  const state = new RuntimeStateStore(options.storage);
209
217
  const log = new StderrLogger();
210
218
  const signal = options.signal ?? new AbortController().signal;
219
+ const currentRunId = options.runId || randomUUID();
220
+ const currentRootRunId = options.rootRunId || options.parentRunId || currentRunId;
211
221
 
212
222
  const invoker: ActionInvoker = {
213
223
  async invoke<I, O>(action: ActionDefinition<I, O>, input: I): Promise<O> {
@@ -215,18 +225,30 @@ export function createActionContext(options: ContextOptions): ActionContext {
215
225
  return (await options.onActionInvoke(
216
226
  action as any,
217
227
  input,
218
- options.parentRunId
228
+ currentRunId
219
229
  )) as O;
220
230
  }
221
231
  throw new Error("ActionInvoker not configured with an invocation delegate");
222
232
  },
223
233
  };
224
234
 
235
+ const processApi = options.process || getProcessExecutor();
236
+ const progressApi: ProgressReporter = options.progress || {
237
+ report() {},
238
+ };
239
+
225
240
  return {
226
241
  config,
227
242
  state,
228
243
  actions: invoker,
244
+ process: processApi,
229
245
  log,
246
+ progress: progressApi,
230
247
  signal,
248
+ run: {
249
+ id: currentRunId,
250
+ rootId: currentRootRunId,
251
+ parentId: options.parentRunId,
252
+ },
231
253
  };
232
254
  }
@@ -0,0 +1,142 @@
1
+ import type { ExecutionEvent } from "@actiondock/sdk";
2
+
3
+ export interface EventSink {
4
+ emit(event: ExecutionEvent): void;
5
+ subscribe(
6
+ runId: string,
7
+ options?: { after?: number; signal?: AbortSignal }
8
+ ): AsyncIterable<ExecutionEvent>;
9
+ clear(runId: string): void;
10
+ }
11
+
12
+ /**
13
+ * 进程内有界事件缓冲区实现。
14
+ * 遵循设计文档:每个运行最多保留 1024 条或 1MB 事件,支持单调序号。
15
+ */
16
+ export class InMemoryEventSink implements EventSink {
17
+ private eventsByRun = new Map<string, ExecutionEvent[]>();
18
+ private listeners = new Map<string, Set<(event: ExecutionEvent) => void>>();
19
+ private maxEventsPerRun = 1024;
20
+
21
+ emit(event: ExecutionEvent): void {
22
+ let list = this.eventsByRun.get(event.runId);
23
+ if (!list) {
24
+ list = [];
25
+ this.eventsByRun.set(event.runId, list);
26
+ }
27
+
28
+ if (list.length >= this.maxEventsPerRun) {
29
+ // 淘汰旧日志和进度事件,保留状态和结果
30
+ const nonEssentialIndex = list.findIndex((e) => e.type === "log" || e.type === "progress");
31
+ if (nonEssentialIndex >= 0) {
32
+ list.splice(nonEssentialIndex, 1);
33
+ } else {
34
+ list.shift();
35
+ }
36
+ }
37
+ list.push(event);
38
+
39
+ const subs = this.listeners.get(event.runId);
40
+ if (subs) {
41
+ for (const listener of subs) {
42
+ try {
43
+ listener(event);
44
+ } catch {
45
+ // 忽略单个监听器内部异常
46
+ }
47
+ }
48
+ }
49
+ }
50
+
51
+ async *subscribe(
52
+ runId: string,
53
+ options: { after?: number; signal?: AbortSignal } = {}
54
+ ): AsyncIterable<ExecutionEvent> {
55
+ const after = options.after ?? -1;
56
+ const history = this.eventsByRun.get(runId) || [];
57
+
58
+ for (const evt of history) {
59
+ if (evt.sequence > after) {
60
+ yield evt;
61
+ }
62
+ }
63
+
64
+ const lastEvt = history[history.length - 1];
65
+ if (lastEvt && (lastEvt.type === "finish" || lastEvt.type === "status" && lastEvt.status !== "running")) {
66
+ return;
67
+ }
68
+
69
+ const queue: ExecutionEvent[] = [];
70
+ let notify: (() => void) | null = null;
71
+ let done = false;
72
+
73
+ const listener = (evt: ExecutionEvent) => {
74
+ if (evt.sequence > after) {
75
+ queue.push(evt);
76
+ if (notify) {
77
+ notify();
78
+ notify = null;
79
+ }
80
+ if (evt.type === "finish") {
81
+ done = true;
82
+ }
83
+ }
84
+ };
85
+
86
+ let subs = this.listeners.get(runId);
87
+ if (!subs) {
88
+ subs = new Set();
89
+ this.listeners.set(runId, subs);
90
+ }
91
+ subs.add(listener);
92
+
93
+ const cleanup = () => {
94
+ subs?.delete(listener);
95
+ if (subs && subs.size === 0) {
96
+ this.listeners.delete(runId);
97
+ }
98
+ };
99
+
100
+ if (options.signal) {
101
+ options.signal.addEventListener("abort", () => {
102
+ done = true;
103
+ if (notify) {
104
+ notify();
105
+ notify = null;
106
+ }
107
+ }, { once: true });
108
+ }
109
+
110
+ try {
111
+ while (!done && !options.signal?.aborted) {
112
+ if (queue.length > 0) {
113
+ yield queue.shift()!;
114
+ } else {
115
+ await new Promise<void>((resolve) => {
116
+ notify = resolve;
117
+ });
118
+ }
119
+ }
120
+ while (queue.length > 0) {
121
+ yield queue.shift()!;
122
+ }
123
+ } finally {
124
+ cleanup();
125
+ }
126
+ }
127
+
128
+ clear(runId: string): void {
129
+ this.eventsByRun.delete(runId);
130
+ this.listeners.delete(runId);
131
+ }
132
+ }
133
+
134
+ let defaultEventSink: EventSink = new InMemoryEventSink();
135
+
136
+ export function getDefaultEventSink(): EventSink {
137
+ return defaultEventSink;
138
+ }
139
+
140
+ export function setDefaultEventSink(sink: EventSink): void {
141
+ defaultEventSink = sink;
142
+ }
@@ -3,3 +3,7 @@ export * from "./runner";
3
3
  export * from "./execution-manager";
4
4
  export * from "./standalone";
5
5
  export * from "./env";
6
+ export * from "./clock";
7
+ export * from "./process";
8
+ export * from "./events";
9
+