@superdurable/dex-agents 0.0.0-stage → 0.2.0

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.
Files changed (171) hide show
  1. package/LICENSE +105 -0
  2. package/README.md +1981 -2
  3. package/dist/src/agent-built-in-tools.d.ts +81 -0
  4. package/dist/src/agent-built-in-tools.js +246 -0
  5. package/dist/src/agent-built-in-tools.js.map +1 -0
  6. package/dist/src/agent-context-compaction.d.ts +108 -0
  7. package/dist/src/agent-context-compaction.js +39 -0
  8. package/dist/src/agent-context-compaction.js.map +1 -0
  9. package/dist/src/agent-event-writer.d.ts +26 -0
  10. package/dist/src/agent-event-writer.js +110 -0
  11. package/dist/src/agent-event-writer.js.map +1 -0
  12. package/dist/src/agent-framework-runner.d.ts +808 -0
  13. package/dist/src/agent-framework-runner.js +82 -0
  14. package/dist/src/agent-framework-runner.js.map +1 -0
  15. package/dist/src/agent-plans.d.ts +48 -0
  16. package/dist/src/agent-plans.js +86 -0
  17. package/dist/src/agent-plans.js.map +1 -0
  18. package/dist/src/agent-session-cancellation-steps.d.ts +28 -0
  19. package/dist/src/agent-session-cancellation-steps.js +154 -0
  20. package/dist/src/agent-session-cancellation-steps.js.map +1 -0
  21. package/dist/src/agent-session-codecs.d.ts +105 -0
  22. package/dist/src/agent-session-codecs.js +689 -0
  23. package/dist/src/agent-session-codecs.js.map +1 -0
  24. package/dist/src/agent-session-flow.d.ts +416 -0
  25. package/dist/src/agent-session-flow.js +623 -0
  26. package/dist/src/agent-session-flow.js.map +1 -0
  27. package/dist/src/agent-session-persistence.d.ts +133 -0
  28. package/dist/src/agent-session-persistence.js +74 -0
  29. package/dist/src/agent-session-persistence.js.map +1 -0
  30. package/dist/src/agent-session-settings.d.ts +58 -0
  31. package/dist/src/agent-session-settings.js +81 -0
  32. package/dist/src/agent-session-settings.js.map +1 -0
  33. package/dist/src/agent-session-step-options.d.ts +27 -0
  34. package/dist/src/agent-session-step-options.js +39 -0
  35. package/dist/src/agent-session-step-options.js.map +1 -0
  36. package/dist/src/agent-session-step-types.d.ts +20 -0
  37. package/dist/src/agent-session-step-types.js +28 -0
  38. package/dist/src/agent-session-step-types.js.map +1 -0
  39. package/dist/src/agent-session-steps.d.ts +179 -0
  40. package/dist/src/agent-session-steps.js +1272 -0
  41. package/dist/src/agent-session-steps.js.map +1 -0
  42. package/dist/src/agent-session-tracer.d.ts +61 -0
  43. package/dist/src/agent-session-tracer.js +608 -0
  44. package/dist/src/agent-session-tracer.js.map +1 -0
  45. package/dist/src/agent-session-transitions.d.ts +224 -0
  46. package/dist/src/agent-session-transitions.js +850 -0
  47. package/dist/src/agent-session-transitions.js.map +1 -0
  48. package/dist/src/agent-session-values.d.ts +998 -0
  49. package/dist/src/agent-session-values.js +162 -0
  50. package/dist/src/agent-session-values.js.map +1 -0
  51. package/dist/src/agent-session-wait-steps.d.ts +53 -0
  52. package/dist/src/agent-session-wait-steps.js +389 -0
  53. package/dist/src/agent-session-wait-steps.js.map +1 -0
  54. package/dist/src/agent-tracing-options.d.ts +107 -0
  55. package/dist/src/agent-tracing-options.js +20 -0
  56. package/dist/src/agent-tracing-options.js.map +1 -0
  57. package/dist/src/ai-sdk/ai-sdk-model-client.d.ts +40 -0
  58. package/dist/src/ai-sdk/ai-sdk-model-client.js +235 -0
  59. package/dist/src/ai-sdk/ai-sdk-model-client.js.map +1 -0
  60. package/dist/src/ai-sdk/index.d.ts +1 -0
  61. package/dist/src/ai-sdk/index.js +9 -0
  62. package/dist/src/ai-sdk/index.js.map +1 -0
  63. package/dist/src/index.d.ts +10 -0
  64. package/dist/src/index.js +17 -0
  65. package/dist/src/index.js.map +1 -0
  66. package/dist/src/mcp/index.d.ts +3 -0
  67. package/dist/src/mcp/index.js +10 -0
  68. package/dist/src/mcp/index.js.map +1 -0
  69. package/dist/src/mcp/mcp-resource-and-prompt-tools.d.ts +29 -0
  70. package/dist/src/mcp/mcp-resource-and-prompt-tools.js +185 -0
  71. package/dist/src/mcp/mcp-resource-and-prompt-tools.js.map +1 -0
  72. package/dist/src/mcp/mcp-server-config.d.ts +86 -0
  73. package/dist/src/mcp/mcp-server-config.js +38 -0
  74. package/dist/src/mcp/mcp-server-config.js.map +1 -0
  75. package/dist/src/mcp/mcp-tool-source.d.ts +470 -0
  76. package/dist/src/mcp/mcp-tool-source.js +1204 -0
  77. package/dist/src/mcp/mcp-tool-source.js.map +1 -0
  78. package/dist/src/stepwise/agent-definition.d.ts +158 -0
  79. package/dist/src/stepwise/agent-definition.js +9 -0
  80. package/dist/src/stepwise/agent-definition.js.map +1 -0
  81. package/dist/src/stepwise/function-tool.d.ts +130 -0
  82. package/dist/src/stepwise/function-tool.js +75 -0
  83. package/dist/src/stepwise/function-tool.js.map +1 -0
  84. package/dist/src/stepwise/index.d.ts +7 -0
  85. package/dist/src/stepwise/index.js +12 -0
  86. package/dist/src/stepwise/index.js.map +1 -0
  87. package/dist/src/stepwise/json-schema-validation.d.ts +9 -0
  88. package/dist/src/stepwise/json-schema-validation.js +141 -0
  89. package/dist/src/stepwise/json-schema-validation.js.map +1 -0
  90. package/dist/src/stepwise/model-client.d.ts +194 -0
  91. package/dist/src/stepwise/model-client.js +9 -0
  92. package/dist/src/stepwise/model-client.js.map +1 -0
  93. package/dist/src/stepwise/scripted-model-client.d.ts +99 -0
  94. package/dist/src/stepwise/scripted-model-client.js +156 -0
  95. package/dist/src/stepwise/scripted-model-client.js.map +1 -0
  96. package/dist/src/stepwise/stepwise-agent-runner.d.ts +189 -0
  97. package/dist/src/stepwise/stepwise-agent-runner.js +804 -0
  98. package/dist/src/stepwise/stepwise-agent-runner.js.map +1 -0
  99. package/dist/src/stepwise/stepwise-agent-session-flow.d.ts +35 -0
  100. package/dist/src/stepwise/stepwise-agent-session-flow.js +56 -0
  101. package/dist/src/stepwise/stepwise-agent-session-flow.js.map +1 -0
  102. package/dist/src/stepwise/stepwise-input-items.d.ts +12 -0
  103. package/dist/src/stepwise/stepwise-input-items.js +172 -0
  104. package/dist/src/stepwise/stepwise-input-items.js.map +1 -0
  105. package/dist/src/stepwise/tool-source.d.ts +103 -0
  106. package/dist/src/stepwise/tool-source.js +9 -0
  107. package/dist/src/stepwise/tool-source.js.map +1 -0
  108. package/dist/src/testing/agent-session-test-client.d.ts +205 -0
  109. package/dist/src/testing/agent-session-test-client.js +290 -0
  110. package/dist/src/testing/agent-session-test-client.js.map +1 -0
  111. package/dist/src/testing/agent-test-environment.d.ts +52 -0
  112. package/dist/src/testing/agent-test-environment.js +86 -0
  113. package/dist/src/testing/agent-test-environment.js.map +1 -0
  114. package/dist/src/testing/agent-worker-process.d.ts +91 -0
  115. package/dist/src/testing/agent-worker-process.js +150 -0
  116. package/dist/src/testing/agent-worker-process.js.map +1 -0
  117. package/dist/src/testing/child-process-group.d.ts +33 -0
  118. package/dist/src/testing/child-process-group.js +164 -0
  119. package/dist/src/testing/child-process-group.js.map +1 -0
  120. package/dist/src/testing/coordination.d.ts +99 -0
  121. package/dist/src/testing/coordination.js +155 -0
  122. package/dist/src/testing/coordination.js.map +1 -0
  123. package/dist/src/testing/index.d.ts +8 -0
  124. package/dist/src/testing/index.js +16 -0
  125. package/dist/src/testing/index.js.map +1 -0
  126. package/dist/src/testing/polling.d.ts +31 -0
  127. package/dist/src/testing/polling.js +51 -0
  128. package/dist/src/testing/polling.js.map +1 -0
  129. package/dist/src/testing/private-dex-engine.d.ts +101 -0
  130. package/dist/src/testing/private-dex-engine.js +223 -0
  131. package/dist/src/testing/private-dex-engine.js.map +1 -0
  132. package/dist/src/testing/scripted-model-client.d.ts +1 -0
  133. package/dist/src/testing/scripted-model-client.js +10 -0
  134. package/dist/src/testing/scripted-model-client.js.map +1 -0
  135. package/dist/src/testing/step-history.d.ts +40 -0
  136. package/dist/src/testing/step-history.js +59 -0
  137. package/dist/src/testing/step-history.js.map +1 -0
  138. package/dist/src/ui-stream/agent-event-json.d.ts +30 -0
  139. package/dist/src/ui-stream/agent-event-json.js +64 -0
  140. package/dist/src/ui-stream/agent-event-json.js.map +1 -0
  141. package/dist/src/ui-stream/agent-event-sse.d.ts +154 -0
  142. package/dist/src/ui-stream/agent-event-sse.js +159 -0
  143. package/dist/src/ui-stream/agent-event-sse.js.map +1 -0
  144. package/dist/src/ui-stream/agent-event-stream-positions.d.ts +42 -0
  145. package/dist/src/ui-stream/agent-event-stream-positions.js +153 -0
  146. package/dist/src/ui-stream/agent-event-stream-positions.js.map +1 -0
  147. package/dist/src/ui-stream/agent-session-record-reader.d.ts +72 -0
  148. package/dist/src/ui-stream/agent-session-record-reader.js +241 -0
  149. package/dist/src/ui-stream/agent-session-record-reader.js.map +1 -0
  150. package/dist/src/ui-stream/agent-ui-message-stream-responses.d.ts +219 -0
  151. package/dist/src/ui-stream/agent-ui-message-stream-responses.js +440 -0
  152. package/dist/src/ui-stream/agent-ui-message-stream-responses.js.map +1 -0
  153. package/dist/src/ui-stream/agent-ui-message-stream.d.ts +138 -0
  154. package/dist/src/ui-stream/agent-ui-message-stream.js +681 -0
  155. package/dist/src/ui-stream/agent-ui-message-stream.js.map +1 -0
  156. package/dist/src/ui-stream/agent-ui-message-types.d.ts +238 -0
  157. package/dist/src/ui-stream/agent-ui-message-types.js +57 -0
  158. package/dist/src/ui-stream/agent-ui-message-types.js.map +1 -0
  159. package/dist/src/ui-stream/agent-ui-messages.d.ts +66 -0
  160. package/dist/src/ui-stream/agent-ui-messages.js +220 -0
  161. package/dist/src/ui-stream/agent-ui-messages.js.map +1 -0
  162. package/dist/src/ui-stream/agent-ui-stream-cursor.d.ts +64 -0
  163. package/dist/src/ui-stream/agent-ui-stream-cursor.js +186 -0
  164. package/dist/src/ui-stream/agent-ui-stream-cursor.js.map +1 -0
  165. package/dist/src/ui-stream/index.d.ts +14 -0
  166. package/dist/src/ui-stream/index.js +22 -0
  167. package/dist/src/ui-stream/index.js.map +1 -0
  168. package/dist/src/ui-stream/server-sent-event-responses.d.ts +6 -0
  169. package/dist/src/ui-stream/server-sent-event-responses.js +34 -0
  170. package/dist/src/ui-stream/server-sent-event-responses.js.map +1 -0
  171. package/package.json +86 -4
@@ -0,0 +1,91 @@
1
+ import { type Flow } from "@superdurable/dex";
2
+ /** Environment variable that carries the Dex Server address into a Worker child process. */
3
+ export declare const agentWorkerServerAddressVariable = "DEX_AGENTS_SERVER_ADDRESS";
4
+ /**
5
+ * Environment variable naming the BlobCache directory of a Worker child process. When it is unset,
6
+ * the child uses a new temporary directory and deletes it on exit.
7
+ */
8
+ export declare const agentWorkerBlobCacheDirectoryVariable = "DEX_AGENTS_BLOB_CACHE_DIR";
9
+ /** Line a Worker child process prints once every queue is registered; the same as the Python core. */
10
+ export declare const agentWorkerReadyLine = "dex-agents test Worker ready";
11
+ /** Options of {@link KillableWorkerProcess.start}. */
12
+ export interface KillableWorkerProcessOptions {
13
+ /**
14
+ * Path of the compiled ES module the child runs with `node`. The module must call
15
+ * {@link runAgentWorkerProcess} with the Flows it serves.
16
+ */
17
+ readonly entryModulePath: string;
18
+ /** Dex Server address, usually {@link PrivateDexEngine.serverAddress}. */
19
+ readonly serverAddress: string;
20
+ /** Log file; output of every start is appended to it. */
21
+ readonly logPath: string;
22
+ /** Extra environment variables for the child, such as a directory for tool marker files. */
23
+ readonly environment?: Readonly<Record<string, string>>;
24
+ /** Milliseconds to wait for {@link agentWorkerReadyLine}. Defaults to 60,000. */
25
+ readonly readyTimeoutMs?: number;
26
+ }
27
+ /**
28
+ * A Dex Worker running in a child process that a test can hard-kill.
29
+ *
30
+ * The child leads its own process group. {@link kill} sends SIGKILL to the group, so the Worker
31
+ * gets no chance to drain, finish a Step, or close its streams, exactly like a crashed host.
32
+ * Start a new instance afterward to model the replacement Worker.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * // worker-main.ts, compiled to dist/test/worker-main.js
37
+ * import { runAgentWorkerProcess } from "@superdurable/dex-agents/testing";
38
+ * await runAgentWorkerProcess([createMyAgentFlow()]);
39
+ *
40
+ * // In the test
41
+ * const worker = await KillableWorkerProcess.start({
42
+ * entryModulePath: fileURLToPath(new URL("./worker-main.js", import.meta.url)),
43
+ * serverAddress: engine.serverAddress,
44
+ * logPath: join(engine.stateDirectory, "worker.log"),
45
+ * });
46
+ * await worker.kill();
47
+ * ```
48
+ */
49
+ export declare class KillableWorkerProcess {
50
+ private readonly processGroup;
51
+ private constructor();
52
+ /**
53
+ * Starts the child and waits until its Worker registered every pull queue.
54
+ * @param options - Entry module, server address, log path, and environment.
55
+ * @returns The running Worker process.
56
+ * @throws {Error} When the child exits or is not ready in time; the message holds its log.
57
+ */
58
+ static start(options: KillableWorkerProcessOptions): Promise<KillableWorkerProcess>;
59
+ /** Process ID of the child. */
60
+ get pid(): number;
61
+ /** Whether the child is still running. */
62
+ get isRunning(): boolean;
63
+ /**
64
+ * Returns the child's output since this start.
65
+ * @returns Standard output and standard error, interleaved.
66
+ */
67
+ readLog(): string;
68
+ /**
69
+ * Sends SIGKILL to the child's process group and waits for it to exit. Safe to call again.
70
+ * @returns A Promise that resolves after the child exited.
71
+ */
72
+ kill(): Promise<void>;
73
+ /**
74
+ * Asks the child to drain and exit with SIGTERM, then sends SIGKILL after 15 seconds.
75
+ * @returns A Promise that resolves after the child exited.
76
+ */
77
+ stop(): Promise<void>;
78
+ }
79
+ /**
80
+ * Runs a pull Worker for the given Flows until SIGTERM or SIGINT; call it from a Worker child module.
81
+ *
82
+ * It reads the server address from {@link agentWorkerServerAddressVariable} and the BlobCache directory
83
+ * from {@link agentWorkerBlobCacheDirectoryVariable}, uses a fresh app Worker ID and 8 concurrent tasks, waits for pull
84
+ * registration, then prints {@link agentWorkerReadyLine} to standard output. On SIGTERM or SIGINT it
85
+ * closes the Worker and exits with status 0.
86
+ *
87
+ * @param flows - Flows the Worker serves; build them exactly as the test's Client Registry does.
88
+ * @returns A Promise that never resolves; the process exits on a signal.
89
+ * @throws {Error} When the server address is missing or the Worker cannot register.
90
+ */
91
+ export declare function runAgentWorkerProcess(flows: readonly Flow<any>[]): Promise<never>;
@@ -0,0 +1,150 @@
1
+ // Copyright (c) 2026 Super Durable, Inc.
2
+ //
3
+ // Licensed under the Sustainable Use License 1.0.
4
+ // You may not use this file except in compliance with the License.
5
+ // See the LICENSE file in the repository root.
6
+ //
7
+ // SPDX-License-Identifier: LicenseRef-Sustainable-Use-1.0
8
+ import { randomUUID } from "node:crypto";
9
+ import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { join } from "node:path";
12
+ import { Registry, Worker, openBlobCache } from "@superdurable/dex";
13
+ import { ChildProcessGroup } from "./child-process-group.js";
14
+ /** Environment variable that carries the Dex Server address into a Worker child process. */
15
+ export const agentWorkerServerAddressVariable = "DEX_AGENTS_SERVER_ADDRESS";
16
+ /**
17
+ * Environment variable naming the BlobCache directory of a Worker child process. When it is unset,
18
+ * the child uses a new temporary directory and deletes it on exit.
19
+ */
20
+ export const agentWorkerBlobCacheDirectoryVariable = "DEX_AGENTS_BLOB_CACHE_DIR";
21
+ /** Line a Worker child process prints once every queue is registered; the same as the Python core. */
22
+ export const agentWorkerReadyLine = "dex-agents test Worker ready";
23
+ /**
24
+ * A Dex Worker running in a child process that a test can hard-kill.
25
+ *
26
+ * The child leads its own process group. {@link kill} sends SIGKILL to the group, so the Worker
27
+ * gets no chance to drain, finish a Step, or close its streams, exactly like a crashed host.
28
+ * Start a new instance afterward to model the replacement Worker.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * // worker-main.ts, compiled to dist/test/worker-main.js
33
+ * import { runAgentWorkerProcess } from "@superdurable/dex-agents/testing";
34
+ * await runAgentWorkerProcess([createMyAgentFlow()]);
35
+ *
36
+ * // In the test
37
+ * const worker = await KillableWorkerProcess.start({
38
+ * entryModulePath: fileURLToPath(new URL("./worker-main.js", import.meta.url)),
39
+ * serverAddress: engine.serverAddress,
40
+ * logPath: join(engine.stateDirectory, "worker.log"),
41
+ * });
42
+ * await worker.kill();
43
+ * ```
44
+ */
45
+ export class KillableWorkerProcess {
46
+ processGroup;
47
+ constructor(processGroup) {
48
+ this.processGroup = processGroup;
49
+ }
50
+ /**
51
+ * Starts the child and waits until its Worker registered every pull queue.
52
+ * @param options - Entry module, server address, log path, and environment.
53
+ * @returns The running Worker process.
54
+ * @throws {Error} When the child exits or is not ready in time; the message holds its log.
55
+ */
56
+ static async start(options) {
57
+ const processGroup = await ChildProcessGroup.start({
58
+ description: `Worker ${options.entryModulePath}`,
59
+ command: process.execPath,
60
+ commandArguments: [options.entryModulePath],
61
+ environment: {
62
+ ...process.env,
63
+ ...options.environment,
64
+ [agentWorkerServerAddressVariable]: options.serverAddress,
65
+ },
66
+ logPath: options.logPath,
67
+ readyLine: agentWorkerReadyLine,
68
+ readyTimeoutMs: options.readyTimeoutMs ?? 60_000,
69
+ });
70
+ return new KillableWorkerProcess(processGroup);
71
+ }
72
+ /** Process ID of the child. */
73
+ get pid() {
74
+ return this.processGroup.pid;
75
+ }
76
+ /** Whether the child is still running. */
77
+ get isRunning() {
78
+ return this.processGroup.isRunning;
79
+ }
80
+ /**
81
+ * Returns the child's output since this start.
82
+ * @returns Standard output and standard error, interleaved.
83
+ */
84
+ readLog() {
85
+ return this.processGroup.readLog();
86
+ }
87
+ /**
88
+ * Sends SIGKILL to the child's process group and waits for it to exit. Safe to call again.
89
+ * @returns A Promise that resolves after the child exited.
90
+ */
91
+ kill() {
92
+ return this.processGroup.kill();
93
+ }
94
+ /**
95
+ * Asks the child to drain and exit with SIGTERM, then sends SIGKILL after 15 seconds.
96
+ * @returns A Promise that resolves after the child exited.
97
+ */
98
+ stop() {
99
+ return this.processGroup.stop();
100
+ }
101
+ }
102
+ /**
103
+ * Runs a pull Worker for the given Flows until SIGTERM or SIGINT; call it from a Worker child module.
104
+ *
105
+ * It reads the server address from {@link agentWorkerServerAddressVariable} and the BlobCache directory
106
+ * from {@link agentWorkerBlobCacheDirectoryVariable}, uses a fresh app Worker ID and 8 concurrent tasks, waits for pull
107
+ * registration, then prints {@link agentWorkerReadyLine} to standard output. On SIGTERM or SIGINT it
108
+ * closes the Worker and exits with status 0.
109
+ *
110
+ * @param flows - Flows the Worker serves; build them exactly as the test's Client Registry does.
111
+ * @returns A Promise that never resolves; the process exits on a signal.
112
+ * @throws {Error} When the server address is missing or the Worker cannot register.
113
+ */
114
+ export async function runAgentWorkerProcess(flows) {
115
+ const serverAddress = process.env[agentWorkerServerAddressVariable];
116
+ if (serverAddress === undefined || serverAddress === "") {
117
+ throw new Error(`${agentWorkerServerAddressVariable} is not set`);
118
+ }
119
+ const configuredCacheDirectory = process.env[agentWorkerBlobCacheDirectoryVariable];
120
+ const isCacheDirectoryOwned = configuredCacheDirectory === undefined || configuredCacheDirectory === "";
121
+ const cacheDirectory = isCacheDirectoryOwned
122
+ ? mkdtempSync(join(tmpdir(), "dex-agents-worker-cache-"))
123
+ : configuredCacheDirectory;
124
+ mkdirSync(cacheDirectory, { recursive: true });
125
+ const blobCache = openBlobCache({ directory: cacheDirectory, maxBytes: 64 * 1024 * 1024 });
126
+ const worker = new Worker(new Registry(flows), blobCache, {
127
+ serverAddress,
128
+ // Parallel tool Steps must not depend on the CI machine's CPU count.
129
+ pull: { appWorkerID: `dex-agents-test-worker-${randomUUID()}`, maxConcurrentTasks: 8 },
130
+ });
131
+ const closeAndExit = async () => {
132
+ try {
133
+ await worker.close();
134
+ blobCache.close();
135
+ }
136
+ finally {
137
+ if (isCacheDirectoryOwned) {
138
+ rmSync(cacheDirectory, { recursive: true, force: true });
139
+ }
140
+ process.exit(0);
141
+ }
142
+ };
143
+ process.once("SIGTERM", () => void closeAndExit());
144
+ process.once("SIGINT", () => void closeAndExit());
145
+ await worker.start();
146
+ await worker.waitPullRegistered({ timeoutMs: 30_000 });
147
+ process.stdout.write(`${agentWorkerReadyLine}\n`);
148
+ return new Promise(() => undefined);
149
+ }
150
+ //# sourceMappingURL=agent-worker-process.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-worker-process.js","sourceRoot":"","sources":["../../../src/testing/agent-worker-process.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /** Command, environment, and readiness of one child process group. */
2
+ export interface ChildProcessGroupLaunch {
3
+ readonly description: string;
4
+ readonly command: string;
5
+ readonly commandArguments: readonly string[];
6
+ readonly environment: NodeJS.ProcessEnv;
7
+ readonly logPath: string;
8
+ readonly readyLine: string;
9
+ readonly readyTimeoutMs: number;
10
+ }
11
+ /** A child process that leads its own process group, so signals reach every descendant. */
12
+ export declare class ChildProcessGroup {
13
+ private readonly launch;
14
+ private readonly child;
15
+ private readonly logOffset;
16
+ private readonly exited;
17
+ private hasExited;
18
+ private spawnError;
19
+ private constructor();
20
+ static start(launch: ChildProcessGroupLaunch): Promise<ChildProcessGroup>;
21
+ /** Process ID of the group leader. */
22
+ get pid(): number;
23
+ get isRunning(): boolean;
24
+ /** Output written since this process started. */
25
+ readLog(): string;
26
+ /** Sends SIGKILL to the whole group and waits for the leader to exit. */
27
+ kill(): Promise<void>;
28
+ /** Sends SIGTERM to the whole group, then SIGKILL after 15 seconds. */
29
+ stop(): Promise<void>;
30
+ signalGroupSynchronously(signal: NodeJS.Signals): void;
31
+ private signalGroup;
32
+ private waitForReadyLine;
33
+ }
@@ -0,0 +1,164 @@
1
+ // Copyright (c) 2026 Super Durable, Inc.
2
+ //
3
+ // Licensed under the Sustainable Use License 1.0.
4
+ // You may not use this file except in compliance with the License.
5
+ // See the LICENSE file in the repository root.
6
+ //
7
+ // SPDX-License-Identifier: LicenseRef-Sustainable-Use-1.0
8
+ import { spawn } from "node:child_process";
9
+ import { closeSync, openSync, readFileSync, statSync } from "node:fs";
10
+ import { setTimeout as delay } from "node:timers/promises";
11
+ const readyPollIntervalMs = 50;
12
+ const gracefulStopTimeoutMs = 15_000;
13
+ const runningProcessGroups = new Set();
14
+ let isExitCleanupRegistered = false;
15
+ /** A child process that leads its own process group, so signals reach every descendant. */
16
+ export class ChildProcessGroup {
17
+ launch;
18
+ child;
19
+ logOffset;
20
+ exited;
21
+ hasExited = false;
22
+ spawnError;
23
+ constructor(launch, child, logOffset) {
24
+ this.launch = launch;
25
+ this.child = child;
26
+ this.logOffset = logOffset;
27
+ this.exited = new Promise((resolve) => {
28
+ const markExited = () => {
29
+ this.hasExited = true;
30
+ runningProcessGroups.delete(this);
31
+ resolve();
32
+ };
33
+ child.once("exit", markExited);
34
+ child.once("error", (error) => {
35
+ this.spawnError = error;
36
+ // A process that never spawned emits no exit event.
37
+ if (child.pid === undefined) {
38
+ markExited();
39
+ }
40
+ });
41
+ });
42
+ }
43
+ static async start(launch) {
44
+ registerExitCleanup();
45
+ const logOffset = fileSize(launch.logPath);
46
+ const logDescriptor = openSync(launch.logPath, "a");
47
+ let child;
48
+ try {
49
+ child = spawn(launch.command, [...launch.commandArguments], {
50
+ detached: true,
51
+ env: launch.environment,
52
+ stdio: ["ignore", logDescriptor, logDescriptor],
53
+ });
54
+ }
55
+ finally {
56
+ closeSync(logDescriptor);
57
+ }
58
+ const processGroup = new ChildProcessGroup(launch, child, logOffset);
59
+ runningProcessGroups.add(processGroup);
60
+ try {
61
+ await processGroup.waitForReadyLine();
62
+ }
63
+ catch (error) {
64
+ await processGroup.kill();
65
+ throw error;
66
+ }
67
+ return processGroup;
68
+ }
69
+ /** Process ID of the group leader. */
70
+ get pid() {
71
+ const pid = this.child.pid;
72
+ if (pid === undefined) {
73
+ throw new Error(`${this.launch.description} has no process ID`);
74
+ }
75
+ return pid;
76
+ }
77
+ get isRunning() {
78
+ return !this.hasExited;
79
+ }
80
+ /** Output written since this process started. */
81
+ readLog() {
82
+ return readFileSync(this.launch.logPath).subarray(this.logOffset).toString("utf8");
83
+ }
84
+ /** Sends SIGKILL to the whole group and waits for the leader to exit. */
85
+ async kill() {
86
+ this.signalGroup("SIGKILL");
87
+ await this.exited;
88
+ }
89
+ /** Sends SIGTERM to the whole group, then SIGKILL after 15 seconds. */
90
+ async stop() {
91
+ this.signalGroup("SIGTERM");
92
+ const gracefulStopTimer = new AbortController();
93
+ const isStopped = await Promise.race([
94
+ this.exited.then(() => true),
95
+ delay(gracefulStopTimeoutMs, false, { signal: gracefulStopTimer.signal }).catch(() => true),
96
+ ]);
97
+ gracefulStopTimer.abort();
98
+ if (!isStopped) {
99
+ await this.kill();
100
+ }
101
+ }
102
+ signalGroupSynchronously(signal) {
103
+ this.signalGroup(signal);
104
+ }
105
+ signalGroup(signal) {
106
+ if (this.hasExited || this.child.pid === undefined) {
107
+ return;
108
+ }
109
+ try {
110
+ process.kill(-this.child.pid, signal);
111
+ }
112
+ catch (error) {
113
+ if (!isNoSuchProcessError(error)) {
114
+ throw error;
115
+ }
116
+ }
117
+ }
118
+ async waitForReadyLine() {
119
+ const deadline = Date.now() + this.launch.readyTimeoutMs;
120
+ while (Date.now() < deadline) {
121
+ const spawnError = this.spawnError;
122
+ if (spawnError !== undefined) {
123
+ throw new Error(`${this.launch.description} failed to start: ${spawnError.message}`, {
124
+ cause: spawnError,
125
+ });
126
+ }
127
+ if (this.readLog().includes(this.launch.readyLine)) {
128
+ return;
129
+ }
130
+ if (this.hasExited) {
131
+ throw new Error(`${this.launch.description} exited before it was ready:\n${this.readLog()}`);
132
+ }
133
+ await delay(readyPollIntervalMs);
134
+ }
135
+ throw new Error(`${this.launch.description} was not ready within ${this.launch.readyTimeoutMs} ms:\n${this.readLog()}`);
136
+ }
137
+ }
138
+ function registerExitCleanup() {
139
+ if (isExitCleanupRegistered) {
140
+ return;
141
+ }
142
+ isExitCleanupRegistered = true;
143
+ // Never leave an engine or Worker behind when the test process exits without cleanup.
144
+ process.once("exit", () => {
145
+ for (const processGroup of runningProcessGroups) {
146
+ processGroup.signalGroupSynchronously("SIGKILL");
147
+ }
148
+ });
149
+ }
150
+ function fileSize(path) {
151
+ try {
152
+ return statSync(path).size;
153
+ }
154
+ catch (error) {
155
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
156
+ return 0;
157
+ }
158
+ throw error;
159
+ }
160
+ }
161
+ function isNoSuchProcessError(error) {
162
+ return error instanceof Error && "code" in error && error.code === "ESRCH";
163
+ }
164
+ //# sourceMappingURL=child-process-group.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"child-process-group.js","sourceRoot":"","sources":["../../../src/testing/child-process-group.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Pauses code in a Worker child at an exact point until the test releases it.
3
+ *
4
+ * The child calls {@link markReached} and then awaits {@link waitForRelease}. The test awaits
5
+ * {@link waitUntilReached} and then kills the Worker, or calls {@link release}. Both sides construct
6
+ * the gate with the same directory and name. The gate survives Worker kills, so a replacement Worker
7
+ * passes a released gate immediately. It mirrors the Python core's `FileGate`.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * // In the tool, inside the Worker child:
12
+ * const gate = new FileGate(stateDirectory, "charge");
13
+ * gate.markReached();
14
+ * await gate.waitForRelease(call.cancellationSignal);
15
+ *
16
+ * // In the test:
17
+ * await new FileGate(stateDirectory, "charge").waitUntilReached();
18
+ * await worker.kill();
19
+ * ```
20
+ */
21
+ export declare class FileGate {
22
+ private readonly reachedPath;
23
+ private readonly releasedPath;
24
+ /**
25
+ * Names a gate inside a directory both processes can read.
26
+ * @param directory - Shared directory, created when needed.
27
+ * @param name - Gate name, unique within the directory.
28
+ */
29
+ constructor(directory: string, name: string);
30
+ /** Records that code reached the gate; repeated calls count each arrival. */
31
+ markReached(): void;
32
+ /**
33
+ * Returns how many times code reached the gate.
34
+ * @returns The number of {@link markReached} calls across all processes.
35
+ */
36
+ reachedCount(): number;
37
+ /**
38
+ * Reports whether the test released the gate.
39
+ * @returns `true` after {@link release}.
40
+ */
41
+ isReleased(): boolean;
42
+ /** Lets current and future waiters pass the gate. */
43
+ release(): void;
44
+ /**
45
+ * Waits until the test releases the gate.
46
+ *
47
+ * It never times out: the test either releases the gate or kills the waiting process. Pass the
48
+ * attempt's cancellation signal so a canceled attempt stops waiting.
49
+ * @param signal - Optional signal that ends the wait with an `AbortError`.
50
+ * @returns A Promise that resolves once the gate is released.
51
+ */
52
+ waitForRelease(signal?: AbortSignal): Promise<void>;
53
+ /**
54
+ * Waits until code reached the gate `count` times.
55
+ * @param count - Arrivals to wait for. Defaults to 1.
56
+ * @param timeoutMs - Maximum wait in milliseconds. Defaults to 90,000.
57
+ * @returns A Promise that resolves once the gate was reached often enough.
58
+ * @throws {Error} When the timeout passes first.
59
+ */
60
+ waitUntilReached(count?: number, timeoutMs?: number): Promise<void>;
61
+ }
62
+ /**
63
+ * Counts how often named operations ran, across Worker processes.
64
+ *
65
+ * A tool in the Worker child records each real run; the test then asserts that a write ran exactly
66
+ * once, even across a Worker kill. Each record is appended synchronously, so it survives a SIGKILL
67
+ * right after. It mirrors the Python core's `InvocationLog`.
68
+ *
69
+ * @example
70
+ * ```ts
71
+ * new InvocationLog(join(stateDirectory, "invocations.log")).record("send_email");
72
+ * assert.equal(new InvocationLog(join(stateDirectory, "invocations.log")).count("send_email"), 1);
73
+ * ```
74
+ */
75
+ export declare class InvocationLog {
76
+ private readonly path;
77
+ /**
78
+ * Uses one append-only file shared by the test and its Workers.
79
+ * @param path - Log file, created on the first record.
80
+ */
81
+ constructor(path: string);
82
+ /**
83
+ * Appends one run of `name`.
84
+ * @param name - Operation name without line breaks.
85
+ * @throws {TypeError} When `name` contains a line break.
86
+ */
87
+ record(name: string): void;
88
+ /**
89
+ * Returns how many runs of `name` were recorded.
90
+ * @param name - Operation name.
91
+ * @returns The number of matching records.
92
+ */
93
+ count(name: string): number;
94
+ /**
95
+ * Returns every recorded name in order.
96
+ * @returns The records, oldest first, or an empty array before the first record.
97
+ */
98
+ names(): string[];
99
+ }