@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.
- package/LICENSE +105 -0
- package/README.md +1981 -2
- package/dist/src/agent-built-in-tools.d.ts +81 -0
- package/dist/src/agent-built-in-tools.js +246 -0
- package/dist/src/agent-built-in-tools.js.map +1 -0
- package/dist/src/agent-context-compaction.d.ts +108 -0
- package/dist/src/agent-context-compaction.js +39 -0
- package/dist/src/agent-context-compaction.js.map +1 -0
- package/dist/src/agent-event-writer.d.ts +26 -0
- package/dist/src/agent-event-writer.js +110 -0
- package/dist/src/agent-event-writer.js.map +1 -0
- package/dist/src/agent-framework-runner.d.ts +808 -0
- package/dist/src/agent-framework-runner.js +82 -0
- package/dist/src/agent-framework-runner.js.map +1 -0
- package/dist/src/agent-plans.d.ts +48 -0
- package/dist/src/agent-plans.js +86 -0
- package/dist/src/agent-plans.js.map +1 -0
- package/dist/src/agent-session-cancellation-steps.d.ts +28 -0
- package/dist/src/agent-session-cancellation-steps.js +154 -0
- package/dist/src/agent-session-cancellation-steps.js.map +1 -0
- package/dist/src/agent-session-codecs.d.ts +105 -0
- package/dist/src/agent-session-codecs.js +689 -0
- package/dist/src/agent-session-codecs.js.map +1 -0
- package/dist/src/agent-session-flow.d.ts +416 -0
- package/dist/src/agent-session-flow.js +623 -0
- package/dist/src/agent-session-flow.js.map +1 -0
- package/dist/src/agent-session-persistence.d.ts +133 -0
- package/dist/src/agent-session-persistence.js +74 -0
- package/dist/src/agent-session-persistence.js.map +1 -0
- package/dist/src/agent-session-settings.d.ts +58 -0
- package/dist/src/agent-session-settings.js +81 -0
- package/dist/src/agent-session-settings.js.map +1 -0
- package/dist/src/agent-session-step-options.d.ts +27 -0
- package/dist/src/agent-session-step-options.js +39 -0
- package/dist/src/agent-session-step-options.js.map +1 -0
- package/dist/src/agent-session-step-types.d.ts +20 -0
- package/dist/src/agent-session-step-types.js +28 -0
- package/dist/src/agent-session-step-types.js.map +1 -0
- package/dist/src/agent-session-steps.d.ts +179 -0
- package/dist/src/agent-session-steps.js +1272 -0
- package/dist/src/agent-session-steps.js.map +1 -0
- package/dist/src/agent-session-tracer.d.ts +61 -0
- package/dist/src/agent-session-tracer.js +608 -0
- package/dist/src/agent-session-tracer.js.map +1 -0
- package/dist/src/agent-session-transitions.d.ts +224 -0
- package/dist/src/agent-session-transitions.js +850 -0
- package/dist/src/agent-session-transitions.js.map +1 -0
- package/dist/src/agent-session-values.d.ts +998 -0
- package/dist/src/agent-session-values.js +162 -0
- package/dist/src/agent-session-values.js.map +1 -0
- package/dist/src/agent-session-wait-steps.d.ts +53 -0
- package/dist/src/agent-session-wait-steps.js +389 -0
- package/dist/src/agent-session-wait-steps.js.map +1 -0
- package/dist/src/agent-tracing-options.d.ts +107 -0
- package/dist/src/agent-tracing-options.js +20 -0
- package/dist/src/agent-tracing-options.js.map +1 -0
- package/dist/src/ai-sdk/ai-sdk-model-client.d.ts +40 -0
- package/dist/src/ai-sdk/ai-sdk-model-client.js +235 -0
- package/dist/src/ai-sdk/ai-sdk-model-client.js.map +1 -0
- package/dist/src/ai-sdk/index.d.ts +1 -0
- package/dist/src/ai-sdk/index.js +9 -0
- package/dist/src/ai-sdk/index.js.map +1 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.js +17 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/mcp/index.d.ts +3 -0
- package/dist/src/mcp/index.js +10 -0
- package/dist/src/mcp/index.js.map +1 -0
- package/dist/src/mcp/mcp-resource-and-prompt-tools.d.ts +29 -0
- package/dist/src/mcp/mcp-resource-and-prompt-tools.js +185 -0
- package/dist/src/mcp/mcp-resource-and-prompt-tools.js.map +1 -0
- package/dist/src/mcp/mcp-server-config.d.ts +86 -0
- package/dist/src/mcp/mcp-server-config.js +38 -0
- package/dist/src/mcp/mcp-server-config.js.map +1 -0
- package/dist/src/mcp/mcp-tool-source.d.ts +470 -0
- package/dist/src/mcp/mcp-tool-source.js +1204 -0
- package/dist/src/mcp/mcp-tool-source.js.map +1 -0
- package/dist/src/stepwise/agent-definition.d.ts +158 -0
- package/dist/src/stepwise/agent-definition.js +9 -0
- package/dist/src/stepwise/agent-definition.js.map +1 -0
- package/dist/src/stepwise/function-tool.d.ts +130 -0
- package/dist/src/stepwise/function-tool.js +75 -0
- package/dist/src/stepwise/function-tool.js.map +1 -0
- package/dist/src/stepwise/index.d.ts +7 -0
- package/dist/src/stepwise/index.js +12 -0
- package/dist/src/stepwise/index.js.map +1 -0
- package/dist/src/stepwise/json-schema-validation.d.ts +9 -0
- package/dist/src/stepwise/json-schema-validation.js +141 -0
- package/dist/src/stepwise/json-schema-validation.js.map +1 -0
- package/dist/src/stepwise/model-client.d.ts +194 -0
- package/dist/src/stepwise/model-client.js +9 -0
- package/dist/src/stepwise/model-client.js.map +1 -0
- package/dist/src/stepwise/scripted-model-client.d.ts +99 -0
- package/dist/src/stepwise/scripted-model-client.js +156 -0
- package/dist/src/stepwise/scripted-model-client.js.map +1 -0
- package/dist/src/stepwise/stepwise-agent-runner.d.ts +189 -0
- package/dist/src/stepwise/stepwise-agent-runner.js +804 -0
- package/dist/src/stepwise/stepwise-agent-runner.js.map +1 -0
- package/dist/src/stepwise/stepwise-agent-session-flow.d.ts +35 -0
- package/dist/src/stepwise/stepwise-agent-session-flow.js +56 -0
- package/dist/src/stepwise/stepwise-agent-session-flow.js.map +1 -0
- package/dist/src/stepwise/stepwise-input-items.d.ts +12 -0
- package/dist/src/stepwise/stepwise-input-items.js +172 -0
- package/dist/src/stepwise/stepwise-input-items.js.map +1 -0
- package/dist/src/stepwise/tool-source.d.ts +103 -0
- package/dist/src/stepwise/tool-source.js +9 -0
- package/dist/src/stepwise/tool-source.js.map +1 -0
- package/dist/src/testing/agent-session-test-client.d.ts +205 -0
- package/dist/src/testing/agent-session-test-client.js +290 -0
- package/dist/src/testing/agent-session-test-client.js.map +1 -0
- package/dist/src/testing/agent-test-environment.d.ts +52 -0
- package/dist/src/testing/agent-test-environment.js +86 -0
- package/dist/src/testing/agent-test-environment.js.map +1 -0
- package/dist/src/testing/agent-worker-process.d.ts +91 -0
- package/dist/src/testing/agent-worker-process.js +150 -0
- package/dist/src/testing/agent-worker-process.js.map +1 -0
- package/dist/src/testing/child-process-group.d.ts +33 -0
- package/dist/src/testing/child-process-group.js +164 -0
- package/dist/src/testing/child-process-group.js.map +1 -0
- package/dist/src/testing/coordination.d.ts +99 -0
- package/dist/src/testing/coordination.js +155 -0
- package/dist/src/testing/coordination.js.map +1 -0
- package/dist/src/testing/index.d.ts +8 -0
- package/dist/src/testing/index.js +16 -0
- package/dist/src/testing/index.js.map +1 -0
- package/dist/src/testing/polling.d.ts +31 -0
- package/dist/src/testing/polling.js +51 -0
- package/dist/src/testing/polling.js.map +1 -0
- package/dist/src/testing/private-dex-engine.d.ts +101 -0
- package/dist/src/testing/private-dex-engine.js +223 -0
- package/dist/src/testing/private-dex-engine.js.map +1 -0
- package/dist/src/testing/scripted-model-client.d.ts +1 -0
- package/dist/src/testing/scripted-model-client.js +10 -0
- package/dist/src/testing/scripted-model-client.js.map +1 -0
- package/dist/src/testing/step-history.d.ts +40 -0
- package/dist/src/testing/step-history.js +59 -0
- package/dist/src/testing/step-history.js.map +1 -0
- package/dist/src/ui-stream/agent-event-json.d.ts +30 -0
- package/dist/src/ui-stream/agent-event-json.js +64 -0
- package/dist/src/ui-stream/agent-event-json.js.map +1 -0
- package/dist/src/ui-stream/agent-event-sse.d.ts +154 -0
- package/dist/src/ui-stream/agent-event-sse.js +159 -0
- package/dist/src/ui-stream/agent-event-sse.js.map +1 -0
- package/dist/src/ui-stream/agent-event-stream-positions.d.ts +42 -0
- package/dist/src/ui-stream/agent-event-stream-positions.js +153 -0
- package/dist/src/ui-stream/agent-event-stream-positions.js.map +1 -0
- package/dist/src/ui-stream/agent-session-record-reader.d.ts +72 -0
- package/dist/src/ui-stream/agent-session-record-reader.js +241 -0
- package/dist/src/ui-stream/agent-session-record-reader.js.map +1 -0
- package/dist/src/ui-stream/agent-ui-message-stream-responses.d.ts +219 -0
- package/dist/src/ui-stream/agent-ui-message-stream-responses.js +440 -0
- package/dist/src/ui-stream/agent-ui-message-stream-responses.js.map +1 -0
- package/dist/src/ui-stream/agent-ui-message-stream.d.ts +138 -0
- package/dist/src/ui-stream/agent-ui-message-stream.js +681 -0
- package/dist/src/ui-stream/agent-ui-message-stream.js.map +1 -0
- package/dist/src/ui-stream/agent-ui-message-types.d.ts +238 -0
- package/dist/src/ui-stream/agent-ui-message-types.js +57 -0
- package/dist/src/ui-stream/agent-ui-message-types.js.map +1 -0
- package/dist/src/ui-stream/agent-ui-messages.d.ts +66 -0
- package/dist/src/ui-stream/agent-ui-messages.js +220 -0
- package/dist/src/ui-stream/agent-ui-messages.js.map +1 -0
- package/dist/src/ui-stream/agent-ui-stream-cursor.d.ts +64 -0
- package/dist/src/ui-stream/agent-ui-stream-cursor.js +186 -0
- package/dist/src/ui-stream/agent-ui-stream-cursor.js.map +1 -0
- package/dist/src/ui-stream/index.d.ts +14 -0
- package/dist/src/ui-stream/index.js +22 -0
- package/dist/src/ui-stream/index.js.map +1 -0
- package/dist/src/ui-stream/server-sent-event-responses.d.ts +6 -0
- package/dist/src/ui-stream/server-sent-event-responses.js +34 -0
- package/dist/src/ui-stream/server-sent-event-responses.js.map +1 -0
- package/package.json +86 -4
|
@@ -0,0 +1,155 @@
|
|
|
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 { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
11
|
+
import { pollUntil } from "./polling.js";
|
|
12
|
+
const gatePollIntervalMs = 50;
|
|
13
|
+
/**
|
|
14
|
+
* Pauses code in a Worker child at an exact point until the test releases it.
|
|
15
|
+
*
|
|
16
|
+
* The child calls {@link markReached} and then awaits {@link waitForRelease}. The test awaits
|
|
17
|
+
* {@link waitUntilReached} and then kills the Worker, or calls {@link release}. Both sides construct
|
|
18
|
+
* the gate with the same directory and name. The gate survives Worker kills, so a replacement Worker
|
|
19
|
+
* passes a released gate immediately. It mirrors the Python core's `FileGate`.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* // In the tool, inside the Worker child:
|
|
24
|
+
* const gate = new FileGate(stateDirectory, "charge");
|
|
25
|
+
* gate.markReached();
|
|
26
|
+
* await gate.waitForRelease(call.cancellationSignal);
|
|
27
|
+
*
|
|
28
|
+
* // In the test:
|
|
29
|
+
* await new FileGate(stateDirectory, "charge").waitUntilReached();
|
|
30
|
+
* await worker.kill();
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export class FileGate {
|
|
34
|
+
reachedPath;
|
|
35
|
+
releasedPath;
|
|
36
|
+
/**
|
|
37
|
+
* Names a gate inside a directory both processes can read.
|
|
38
|
+
* @param directory - Shared directory, created when needed.
|
|
39
|
+
* @param name - Gate name, unique within the directory.
|
|
40
|
+
*/
|
|
41
|
+
constructor(directory, name) {
|
|
42
|
+
this.reachedPath = join(directory, `${name}.reached`);
|
|
43
|
+
this.releasedPath = join(directory, `${name}.released`);
|
|
44
|
+
}
|
|
45
|
+
/** Records that code reached the gate; repeated calls count each arrival. */
|
|
46
|
+
markReached() {
|
|
47
|
+
mkdirSync(dirname(this.reachedPath), { recursive: true });
|
|
48
|
+
appendFileSync(this.reachedPath, "reached\n");
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Returns how many times code reached the gate.
|
|
52
|
+
* @returns The number of {@link markReached} calls across all processes.
|
|
53
|
+
*/
|
|
54
|
+
reachedCount() {
|
|
55
|
+
return readLines(this.reachedPath).length;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Reports whether the test released the gate.
|
|
59
|
+
* @returns `true` after {@link release}.
|
|
60
|
+
*/
|
|
61
|
+
isReleased() {
|
|
62
|
+
return existsSync(this.releasedPath);
|
|
63
|
+
}
|
|
64
|
+
/** Lets current and future waiters pass the gate. */
|
|
65
|
+
release() {
|
|
66
|
+
mkdirSync(dirname(this.releasedPath), { recursive: true });
|
|
67
|
+
writeFileSync(this.releasedPath, "");
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Waits until the test releases the gate.
|
|
71
|
+
*
|
|
72
|
+
* It never times out: the test either releases the gate or kills the waiting process. Pass the
|
|
73
|
+
* attempt's cancellation signal so a canceled attempt stops waiting.
|
|
74
|
+
* @param signal - Optional signal that ends the wait with an `AbortError`.
|
|
75
|
+
* @returns A Promise that resolves once the gate is released.
|
|
76
|
+
*/
|
|
77
|
+
async waitForRelease(signal) {
|
|
78
|
+
while (!this.isReleased()) {
|
|
79
|
+
await delay(gatePollIntervalMs, undefined, signal === undefined ? {} : { signal });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Waits until code reached the gate `count` times.
|
|
84
|
+
* @param count - Arrivals to wait for. Defaults to 1.
|
|
85
|
+
* @param timeoutMs - Maximum wait in milliseconds. Defaults to 90,000.
|
|
86
|
+
* @returns A Promise that resolves once the gate was reached often enough.
|
|
87
|
+
* @throws {Error} When the timeout passes first.
|
|
88
|
+
*/
|
|
89
|
+
async waitUntilReached(count = 1, timeoutMs = 90_000) {
|
|
90
|
+
await pollUntil(() => (this.reachedCount() >= count ? true : undefined), {
|
|
91
|
+
description: `gate ${this.reachedPath} reached ${count} time(s)`,
|
|
92
|
+
timeoutMs,
|
|
93
|
+
intervalMs: gatePollIntervalMs,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Counts how often named operations ran, across Worker processes.
|
|
99
|
+
*
|
|
100
|
+
* A tool in the Worker child records each real run; the test then asserts that a write ran exactly
|
|
101
|
+
* once, even across a Worker kill. Each record is appended synchronously, so it survives a SIGKILL
|
|
102
|
+
* right after. It mirrors the Python core's `InvocationLog`.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```ts
|
|
106
|
+
* new InvocationLog(join(stateDirectory, "invocations.log")).record("send_email");
|
|
107
|
+
* assert.equal(new InvocationLog(join(stateDirectory, "invocations.log")).count("send_email"), 1);
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
export class InvocationLog {
|
|
111
|
+
path;
|
|
112
|
+
/**
|
|
113
|
+
* Uses one append-only file shared by the test and its Workers.
|
|
114
|
+
* @param path - Log file, created on the first record.
|
|
115
|
+
*/
|
|
116
|
+
constructor(path) {
|
|
117
|
+
this.path = path;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Appends one run of `name`.
|
|
121
|
+
* @param name - Operation name without line breaks.
|
|
122
|
+
* @throws {TypeError} When `name` contains a line break.
|
|
123
|
+
*/
|
|
124
|
+
record(name) {
|
|
125
|
+
if (/[\r\n]/.test(name)) {
|
|
126
|
+
throw new TypeError("InvocationLog names cannot contain line breaks");
|
|
127
|
+
}
|
|
128
|
+
mkdirSync(dirname(this.path), { recursive: true });
|
|
129
|
+
appendFileSync(this.path, `${name}\n`);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Returns how many runs of `name` were recorded.
|
|
133
|
+
* @param name - Operation name.
|
|
134
|
+
* @returns The number of matching records.
|
|
135
|
+
*/
|
|
136
|
+
count(name) {
|
|
137
|
+
return this.names().filter((recordedName) => recordedName === name).length;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Returns every recorded name in order.
|
|
141
|
+
* @returns The records, oldest first, or an empty array before the first record.
|
|
142
|
+
*/
|
|
143
|
+
names() {
|
|
144
|
+
return readLines(this.path);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function readLines(path) {
|
|
148
|
+
if (!existsSync(path)) {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
return readFileSync(path, "utf8")
|
|
152
|
+
.split("\n")
|
|
153
|
+
.filter((line) => line !== "");
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=coordination.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coordination.js","sourceRoot":"","sources":["../../../src/testing/coordination.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { AgentSessionTestClient, type AgentSessionWaitOptions, } from "./agent-session-test-client.js";
|
|
2
|
+
export { AgentTestEnvironment, type AgentTestEnvironmentOptions, } from "./agent-test-environment.js";
|
|
3
|
+
export { KillableWorkerProcess, agentWorkerBlobCacheDirectoryVariable, agentWorkerReadyLine, agentWorkerServerAddressVariable, runAgentWorkerProcess, type KillableWorkerProcessOptions, } from "./agent-worker-process.js";
|
|
4
|
+
export { FileGate, InvocationLog } from "./coordination.js";
|
|
5
|
+
export { pollUntil, type PollOptions } from "./polling.js";
|
|
6
|
+
export { ScriptedModelClient, scriptedModelApi, scriptedModelProvider, type ScriptedModelReply, type ScriptedModelResponder, type ScriptedToolCall, } from "./scripted-model-client.js";
|
|
7
|
+
export { executeCompletedEvent, readStepHistory, waitForCompletedEvent, type ReadStepHistoryOptions, type StepHistoryEntry, } from "./step-history.js";
|
|
8
|
+
export { PrivateDexEngine, findFreeLoopbackPort, findFreeLoopbackPorts, type PrivateDexEngineOptions, } from "./private-dex-engine.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
export { AgentSessionTestClient, } from "./agent-session-test-client.js";
|
|
9
|
+
export { AgentTestEnvironment, } from "./agent-test-environment.js";
|
|
10
|
+
export { KillableWorkerProcess, agentWorkerBlobCacheDirectoryVariable, agentWorkerReadyLine, agentWorkerServerAddressVariable, runAgentWorkerProcess, } from "./agent-worker-process.js";
|
|
11
|
+
export { FileGate, InvocationLog } from "./coordination.js";
|
|
12
|
+
export { pollUntil } from "./polling.js";
|
|
13
|
+
export { ScriptedModelClient, scriptedModelApi, scriptedModelProvider, } from "./scripted-model-client.js";
|
|
14
|
+
export { executeCompletedEvent, readStepHistory, waitForCompletedEvent, } from "./step-history.js";
|
|
15
|
+
export { PrivateDexEngine, findFreeLoopbackPort, findFreeLoopbackPorts, } from "./private-dex-engine.js";
|
|
16
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/testing/index.ts"],"names":[],"mappings":"AAAA,yCAAyC;AACzC,EAAE;AACF,kDAAkD;AAClD,mEAAmE;AACnE,+CAA+C;AAC/C,EAAE;AACF,0DAA0D;AAE1D,OAAO,EACL,sBAAsB,GAEvB,MAAM,gCAAgC,CAAC;AACxC,OAAO,EACL,oBAAoB,GAErB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,qBAAqB,EACrB,qCAAqC,EACrC,oBAAoB,EACpB,gCAAgC,EAChC,qBAAqB,GAEtB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,EAAE,SAAS,EAAoB,MAAM,cAAc,CAAC;AAC3D,OAAO,EACL,mBAAmB,EACnB,gBAAgB,EAChB,qBAAqB,GAItB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,qBAAqB,EACrB,eAAe,EACf,qBAAqB,GAGtB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,qBAAqB,GAEtB,MAAM,yBAAyB,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Options of {@link pollUntil}. */
|
|
2
|
+
export interface PollOptions {
|
|
3
|
+
/** What is awaited, used in the timeout error. */
|
|
4
|
+
readonly description: string;
|
|
5
|
+
/** Total milliseconds to wait. Defaults to 90,000. */
|
|
6
|
+
readonly timeoutMs?: number;
|
|
7
|
+
/** Milliseconds between probes. Defaults to 200. */
|
|
8
|
+
readonly intervalMs?: number;
|
|
9
|
+
/** Signal that ends the wait early with an `AbortError`. */
|
|
10
|
+
readonly signal?: AbortSignal;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Calls a probe until it returns a value other than `undefined`, or the deadline passes.
|
|
14
|
+
*
|
|
15
|
+
* A probe that throws counts as not ready, so a probe can call an RPC while no Worker runs.
|
|
16
|
+
* Use it only for state that has no direct Client wait.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* const toolCall = await pollUntil(async () => {
|
|
21
|
+
* const [record] = await client.getAttributes(sessionId, flow.persistence.agentToolCalls.instance(id));
|
|
22
|
+
* return record?.status === "outcome_unknown" ? record : undefined;
|
|
23
|
+
* }, { description: "outcome_unknown tool call" });
|
|
24
|
+
* ```
|
|
25
|
+
* @typeParam Value - Value returned by the probe once ready.
|
|
26
|
+
* @param probe - Returns the awaited value, or `undefined` while not ready.
|
|
27
|
+
* @param options - Description, timeout, interval, and signal.
|
|
28
|
+
* @returns The first value the probe returned.
|
|
29
|
+
* @throws {Error} When the deadline passes; the message holds the description and the last probe error.
|
|
30
|
+
*/
|
|
31
|
+
export declare function pollUntil<Value>(probe: () => Value | undefined | Promise<Value | undefined>, options: PollOptions): Promise<Value>;
|
|
@@ -0,0 +1,51 @@
|
|
|
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 { setTimeout as delay } from "node:timers/promises";
|
|
9
|
+
/**
|
|
10
|
+
* Calls a probe until it returns a value other than `undefined`, or the deadline passes.
|
|
11
|
+
*
|
|
12
|
+
* A probe that throws counts as not ready, so a probe can call an RPC while no Worker runs.
|
|
13
|
+
* Use it only for state that has no direct Client wait.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const toolCall = await pollUntil(async () => {
|
|
18
|
+
* const [record] = await client.getAttributes(sessionId, flow.persistence.agentToolCalls.instance(id));
|
|
19
|
+
* return record?.status === "outcome_unknown" ? record : undefined;
|
|
20
|
+
* }, { description: "outcome_unknown tool call" });
|
|
21
|
+
* ```
|
|
22
|
+
* @typeParam Value - Value returned by the probe once ready.
|
|
23
|
+
* @param probe - Returns the awaited value, or `undefined` while not ready.
|
|
24
|
+
* @param options - Description, timeout, interval, and signal.
|
|
25
|
+
* @returns The first value the probe returned.
|
|
26
|
+
* @throws {Error} When the deadline passes; the message holds the description and the last probe error.
|
|
27
|
+
*/
|
|
28
|
+
export async function pollUntil(probe, options) {
|
|
29
|
+
const deadline = Date.now() + (options.timeoutMs ?? 90_000);
|
|
30
|
+
let lastError;
|
|
31
|
+
for (;;) {
|
|
32
|
+
options.signal?.throwIfAborted();
|
|
33
|
+
try {
|
|
34
|
+
const value = await probe();
|
|
35
|
+
if (value !== undefined) {
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
lastError = error;
|
|
41
|
+
}
|
|
42
|
+
if (Date.now() >= deadline) {
|
|
43
|
+
const lastErrorText = lastError === undefined ? "" : `; last probe error: ${String(lastError)}`;
|
|
44
|
+
throw new Error(`timed out waiting for ${options.description}${lastErrorText}`, {
|
|
45
|
+
cause: lastError,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
await delay(options.intervalMs ?? 200, undefined, options.signal === undefined ? {} : { signal: options.signal });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=polling.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"polling.js","sourceRoot":"","sources":["../../../src/testing/polling.ts"],"names":[],"mappings":"AAAA,yCAAyC;AACzC,EAAE;AACF,kDAAkD;AAClD,mEAAmE;AACnE,+CAA+C;AAC/C,EAAE;AACF,0DAA0D;AAE1D,OAAO,EAAE,UAAU,IAAI,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAc3D;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,KAA2D,EAC3D,OAAoB;IAEpB,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,OAAO,CAAC,SAAS,IAAI,MAAM,CAAC,CAAC;IAC5D,IAAI,SAAkB,CAAC;IACvB,SAAS,CAAC;QACR,OAAO,CAAC,MAAM,EAAE,cAAc,EAAE,CAAC;QACjC,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,MAAM,KAAK,EAAE,CAAC;YAC5B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBACxB,OAAO,KAAK,CAAC;YACf,CAAC;QACH,CAAC;QAAC,OAAO,KAAc,EAAE,CAAC;YACxB,SAAS,GAAG,KAAK,CAAC;QACpB,CAAC;QACD,IAAI,IAAI,CAAC,GAAG,EAAE,IAAI,QAAQ,EAAE,CAAC;YAC3B,MAAM,aAAa,GAAG,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,uBAAuB,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC;YAChG,MAAM,IAAI,KAAK,CAAC,yBAAyB,OAAO,CAAC,WAAW,GAAG,aAAa,EAAE,EAAE;gBAC9E,KAAK,EAAE,SAAS;aACjB,CAAC,CAAC;QACL,CAAC;QACD,MAAM,KAAK,CAAC,OAAO,CAAC,UAAU,IAAI,GAAG,EAAE,SAAS,EAAE,OAAO,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACpH,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/** Options of {@link PrivateDexEngine.start}. */
|
|
2
|
+
export interface PrivateDexEngineOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Path to the `dexcli` executable. Defaults to the `DEXCLI` environment variable, then `dexcli`
|
|
5
|
+
* on `PATH`.
|
|
6
|
+
*/
|
|
7
|
+
readonly dexcliPath?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Path to `dex-server-zd`. Defaults to `DEX_SERVER_ZD_BINARY`, then the file next to `dexcli`,
|
|
10
|
+
* then the `dexcli` default.
|
|
11
|
+
*/
|
|
12
|
+
readonly serverZdPath?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Directory for engine state and logs. Defaults to a new temporary directory, which
|
|
15
|
+
* {@link PrivateDexEngine.stop} deletes. A supplied directory is kept.
|
|
16
|
+
*/
|
|
17
|
+
readonly stateDirectory?: string;
|
|
18
|
+
/** Milliseconds to wait for the engine to report readiness. Defaults to 60,000. */
|
|
19
|
+
readonly readyTimeoutMs?: number;
|
|
20
|
+
/**
|
|
21
|
+
* Path of a `dex-server-zd` YAML file with configuration overrides, passed as `dexcli dev -zd-config`.
|
|
22
|
+
* Use it to test server settings, such as `streamStore: { backend: disabled }`, which drops
|
|
23
|
+
* Step Stream writes. When omitted, the engine gets a file that sets only
|
|
24
|
+
* `dev.durabilityMode: processCrashSafe`, which skips fsync and survives a process kill but not a host
|
|
25
|
+
* crash. A file of your own replaces it, so set `dev.durabilityMode` there too, or the engine uses the
|
|
26
|
+
* slower `strict` default of `dex-server-zd` 0.1.18 and later.
|
|
27
|
+
*/
|
|
28
|
+
readonly serverZdConfigPath?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A private `dexcli dev --engine zd` instance on free loopback ports, owned by one test process.
|
|
32
|
+
*
|
|
33
|
+
* Each instance has its own state directory and ports, so tests and other engines on the machine
|
|
34
|
+
* never share state. The engine runs in its own process group; {@link stop} ends `dexcli` and
|
|
35
|
+
* `dex-server-zd` together. Engines still running when the test process exits receive SIGKILL.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* import { before, after } from "node:test";
|
|
40
|
+
* import { PrivateDexEngine } from "@superdurable/dex-agents/testing";
|
|
41
|
+
*
|
|
42
|
+
* let engine: PrivateDexEngine;
|
|
43
|
+
* before(async () => { engine = await PrivateDexEngine.start(); });
|
|
44
|
+
* after(async () => { await engine.stop(); });
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare class PrivateDexEngine {
|
|
48
|
+
/** Dex Server gRPC address, `127.0.0.1:<port>`, for Client and Worker `serverAddress`. */
|
|
49
|
+
readonly serverAddress: string;
|
|
50
|
+
/** Dex Web URL of this engine, useful when debugging a failing test. */
|
|
51
|
+
readonly webUrl: string;
|
|
52
|
+
/** Directory holding the engine state, its log, and test-owned files. */
|
|
53
|
+
readonly stateDirectory: string;
|
|
54
|
+
/** Path of the combined `dexcli` and `dex-server-zd` log. */
|
|
55
|
+
readonly logPath: string;
|
|
56
|
+
private readonly processGroup;
|
|
57
|
+
private readonly isStateDirectoryOwned;
|
|
58
|
+
private constructor();
|
|
59
|
+
/**
|
|
60
|
+
* Starts an engine and waits until it reports readiness.
|
|
61
|
+
*
|
|
62
|
+
* Ports are chosen by the operating system. A start that loses a port race is retried with new
|
|
63
|
+
* ports, up to three times.
|
|
64
|
+
*
|
|
65
|
+
* @param options - Executable paths, state directory, and readiness timeout.
|
|
66
|
+
* @returns The running engine.
|
|
67
|
+
* @throws {Error} When `dexcli` cannot start or never reports readiness; the message holds its log.
|
|
68
|
+
*/
|
|
69
|
+
static start(options?: PrivateDexEngineOptions): Promise<PrivateDexEngine>;
|
|
70
|
+
/**
|
|
71
|
+
* Returns the engine log written since it started.
|
|
72
|
+
* @returns The combined `dexcli` and `dex-server-zd` output.
|
|
73
|
+
*/
|
|
74
|
+
readLog(): string;
|
|
75
|
+
/**
|
|
76
|
+
* Stops the engine and, when the state directory was created by {@link start}, deletes it.
|
|
77
|
+
*
|
|
78
|
+
* Sends SIGTERM to the engine's process group and SIGKILL after 15 seconds. Calling it again is safe.
|
|
79
|
+
* @returns A Promise that resolves after the engine exited.
|
|
80
|
+
*/
|
|
81
|
+
stop(): Promise<void>;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Returns a TCP port on 127.0.0.1 that was free when checked.
|
|
85
|
+
*
|
|
86
|
+
* Another process can still take the port before it is used, so callers retry on bind failures.
|
|
87
|
+
* Use {@link findFreeLoopbackPorts} when a caller needs several ports at once.
|
|
88
|
+
* @returns A port number chosen by the operating system.
|
|
89
|
+
*/
|
|
90
|
+
export declare function findFreeLoopbackPort(): Promise<number>;
|
|
91
|
+
/**
|
|
92
|
+
* Returns distinct TCP ports on 127.0.0.1 that were free when checked.
|
|
93
|
+
*
|
|
94
|
+
* Every listener stays open until all ports are chosen, so the operating system never hands out
|
|
95
|
+
* the same port twice. Another process can still take a port after the listeners close, so callers
|
|
96
|
+
* retry on bind failures.
|
|
97
|
+
* @param count - Number of ports, at least 1.
|
|
98
|
+
* @returns `count` distinct port numbers chosen by the operating system.
|
|
99
|
+
* @throws {RangeError} When `count` is not a positive integer.
|
|
100
|
+
*/
|
|
101
|
+
export declare function findFreeLoopbackPorts(count: number): Promise<number[]>;
|
|
@@ -0,0 +1,223 @@
|
|
|
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 { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { createServer } from "node:net";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { ChildProcessGroup } from "./child-process-group.js";
|
|
13
|
+
const engineReadyLine = "Dex development environment is ready";
|
|
14
|
+
const startAttempts = 3;
|
|
15
|
+
// A config file also works with dexcli releases that predate --durability.
|
|
16
|
+
const processCrashSafeServerZdConfig = "dev:\n durabilityMode: processCrashSafe\n";
|
|
17
|
+
/**
|
|
18
|
+
* A private `dexcli dev --engine zd` instance on free loopback ports, owned by one test process.
|
|
19
|
+
*
|
|
20
|
+
* Each instance has its own state directory and ports, so tests and other engines on the machine
|
|
21
|
+
* never share state. The engine runs in its own process group; {@link stop} ends `dexcli` and
|
|
22
|
+
* `dex-server-zd` together. Engines still running when the test process exits receive SIGKILL.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { before, after } from "node:test";
|
|
27
|
+
* import { PrivateDexEngine } from "@superdurable/dex-agents/testing";
|
|
28
|
+
*
|
|
29
|
+
* let engine: PrivateDexEngine;
|
|
30
|
+
* before(async () => { engine = await PrivateDexEngine.start(); });
|
|
31
|
+
* after(async () => { await engine.stop(); });
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export class PrivateDexEngine {
|
|
35
|
+
serverAddress;
|
|
36
|
+
webUrl;
|
|
37
|
+
stateDirectory;
|
|
38
|
+
logPath;
|
|
39
|
+
processGroup;
|
|
40
|
+
isStateDirectoryOwned;
|
|
41
|
+
constructor(
|
|
42
|
+
/** Dex Server gRPC address, `127.0.0.1:<port>`, for Client and Worker `serverAddress`. */
|
|
43
|
+
serverAddress,
|
|
44
|
+
/** Dex Web URL of this engine, useful when debugging a failing test. */
|
|
45
|
+
webUrl,
|
|
46
|
+
/** Directory holding the engine state, its log, and test-owned files. */
|
|
47
|
+
stateDirectory,
|
|
48
|
+
/** Path of the combined `dexcli` and `dex-server-zd` log. */
|
|
49
|
+
logPath, processGroup, isStateDirectoryOwned) {
|
|
50
|
+
this.serverAddress = serverAddress;
|
|
51
|
+
this.webUrl = webUrl;
|
|
52
|
+
this.stateDirectory = stateDirectory;
|
|
53
|
+
this.logPath = logPath;
|
|
54
|
+
this.processGroup = processGroup;
|
|
55
|
+
this.isStateDirectoryOwned = isStateDirectoryOwned;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Starts an engine and waits until it reports readiness.
|
|
59
|
+
*
|
|
60
|
+
* Ports are chosen by the operating system. A start that loses a port race is retried with new
|
|
61
|
+
* ports, up to three times.
|
|
62
|
+
*
|
|
63
|
+
* @param options - Executable paths, state directory, and readiness timeout.
|
|
64
|
+
* @returns The running engine.
|
|
65
|
+
* @throws {Error} When `dexcli` cannot start or never reports readiness; the message holds its log.
|
|
66
|
+
*/
|
|
67
|
+
static async start(options = {}) {
|
|
68
|
+
const dexcliPath = options.dexcliPath ?? process.env.DEXCLI ?? "dexcli";
|
|
69
|
+
const serverZdPath = options.serverZdPath ?? resolveServerZdPath(dexcliPath);
|
|
70
|
+
const isStateDirectoryOwned = options.stateDirectory === undefined;
|
|
71
|
+
const stateDirectory = options.stateDirectory ?? mkdtempSync(join(tmpdir(), "dex-agents-engine-"));
|
|
72
|
+
mkdirSync(stateDirectory, { recursive: true });
|
|
73
|
+
const logPath = join(stateDirectory, "dexcli.log");
|
|
74
|
+
let configPath = options.serverZdConfigPath;
|
|
75
|
+
if (configPath === undefined) {
|
|
76
|
+
configPath = join(stateDirectory, "zd-overrides.yaml");
|
|
77
|
+
writeFileSync(configPath, processCrashSafeServerZdConfig);
|
|
78
|
+
}
|
|
79
|
+
let lastFailure;
|
|
80
|
+
for (let attempt = 1; attempt <= startAttempts; attempt += 1) {
|
|
81
|
+
const [dexPort, webPort] = await findFreeLoopbackPorts(2);
|
|
82
|
+
if (dexPort === undefined || webPort === undefined) {
|
|
83
|
+
throw new Error("findFreeLoopbackPorts returned too few ports");
|
|
84
|
+
}
|
|
85
|
+
const commandArguments = [
|
|
86
|
+
"dev",
|
|
87
|
+
"-engine",
|
|
88
|
+
"zd",
|
|
89
|
+
"-bind-address",
|
|
90
|
+
"127.0.0.1",
|
|
91
|
+
"-dex-port",
|
|
92
|
+
String(dexPort),
|
|
93
|
+
"-web-port",
|
|
94
|
+
String(webPort),
|
|
95
|
+
"-open=false",
|
|
96
|
+
"-zd-data-dir",
|
|
97
|
+
join(stateDirectory, `zd-${attempt}`),
|
|
98
|
+
"-connector-config-dir",
|
|
99
|
+
join(stateDirectory, "connectors"),
|
|
100
|
+
];
|
|
101
|
+
if (serverZdPath !== undefined) {
|
|
102
|
+
commandArguments.push("-zd-binary", serverZdPath);
|
|
103
|
+
}
|
|
104
|
+
commandArguments.push("-zd-config", configPath);
|
|
105
|
+
try {
|
|
106
|
+
const processGroup = await ChildProcessGroup.start({
|
|
107
|
+
description: "dexcli dev",
|
|
108
|
+
command: dexcliPath,
|
|
109
|
+
commandArguments,
|
|
110
|
+
environment: process.env,
|
|
111
|
+
logPath,
|
|
112
|
+
readyLine: engineReadyLine,
|
|
113
|
+
readyTimeoutMs: options.readyTimeoutMs ?? 60_000,
|
|
114
|
+
});
|
|
115
|
+
return new PrivateDexEngine(`127.0.0.1:${dexPort}`, `http://127.0.0.1:${webPort}`, stateDirectory, logPath, processGroup, isStateDirectoryOwned);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
lastFailure = error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (isStateDirectoryOwned) {
|
|
122
|
+
rmSync(stateDirectory, { recursive: true, force: true });
|
|
123
|
+
}
|
|
124
|
+
throw new Error(`dexcli dev did not start after ${startAttempts} attempts`, {
|
|
125
|
+
cause: lastFailure,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Returns the engine log written since it started.
|
|
130
|
+
* @returns The combined `dexcli` and `dex-server-zd` output.
|
|
131
|
+
*/
|
|
132
|
+
readLog() {
|
|
133
|
+
return this.processGroup.readLog();
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Stops the engine and, when the state directory was created by {@link start}, deletes it.
|
|
137
|
+
*
|
|
138
|
+
* Sends SIGTERM to the engine's process group and SIGKILL after 15 seconds. Calling it again is safe.
|
|
139
|
+
* @returns A Promise that resolves after the engine exited.
|
|
140
|
+
*/
|
|
141
|
+
async stop() {
|
|
142
|
+
await this.processGroup.stop();
|
|
143
|
+
if (this.isStateDirectoryOwned) {
|
|
144
|
+
rmSync(this.stateDirectory, { recursive: true, force: true });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Returns a TCP port on 127.0.0.1 that was free when checked.
|
|
150
|
+
*
|
|
151
|
+
* Another process can still take the port before it is used, so callers retry on bind failures.
|
|
152
|
+
* Use {@link findFreeLoopbackPorts} when a caller needs several ports at once.
|
|
153
|
+
* @returns A port number chosen by the operating system.
|
|
154
|
+
*/
|
|
155
|
+
export async function findFreeLoopbackPort() {
|
|
156
|
+
const [port] = await findFreeLoopbackPorts(1);
|
|
157
|
+
if (port === undefined) {
|
|
158
|
+
throw new Error("no loopback port was chosen");
|
|
159
|
+
}
|
|
160
|
+
return port;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Returns distinct TCP ports on 127.0.0.1 that were free when checked.
|
|
164
|
+
*
|
|
165
|
+
* Every listener stays open until all ports are chosen, so the operating system never hands out
|
|
166
|
+
* the same port twice. Another process can still take a port after the listeners close, so callers
|
|
167
|
+
* retry on bind failures.
|
|
168
|
+
* @param count - Number of ports, at least 1.
|
|
169
|
+
* @returns `count` distinct port numbers chosen by the operating system.
|
|
170
|
+
* @throws {RangeError} When `count` is not a positive integer.
|
|
171
|
+
*/
|
|
172
|
+
export async function findFreeLoopbackPorts(count) {
|
|
173
|
+
if (!Number.isSafeInteger(count) || count <= 0) {
|
|
174
|
+
throw new RangeError(`port count must be a positive integer: ${count}`);
|
|
175
|
+
}
|
|
176
|
+
const servers = [];
|
|
177
|
+
try {
|
|
178
|
+
const ports = [];
|
|
179
|
+
for (let index = 0; index < count; index += 1) {
|
|
180
|
+
const server = createServer();
|
|
181
|
+
servers.push(server);
|
|
182
|
+
ports.push(await listenOnFreeLoopbackPort(server));
|
|
183
|
+
}
|
|
184
|
+
return ports;
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
await Promise.all(servers.map((server) => closeServer(server)));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function listenOnFreeLoopbackPort(server) {
|
|
191
|
+
return new Promise((resolve, reject) => {
|
|
192
|
+
server.once("error", reject);
|
|
193
|
+
server.listen(0, "127.0.0.1", () => {
|
|
194
|
+
const address = server.address();
|
|
195
|
+
if (address === null || typeof address === "string") {
|
|
196
|
+
reject(new Error("loopback listener has no port"));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
resolve(address.port);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
function closeServer(server) {
|
|
204
|
+
return new Promise((resolve) => {
|
|
205
|
+
if (!server.listening) {
|
|
206
|
+
resolve();
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
server.close(() => resolve());
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
function resolveServerZdPath(dexcliPath) {
|
|
213
|
+
const fromEnvironment = process.env.DEX_SERVER_ZD_BINARY;
|
|
214
|
+
if (fromEnvironment !== undefined && fromEnvironment !== "") {
|
|
215
|
+
return fromEnvironment;
|
|
216
|
+
}
|
|
217
|
+
if (!dexcliPath.includes("/")) {
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
const sibling = join(dirname(dexcliPath), "dex-server-zd");
|
|
221
|
+
return existsSync(sibling) ? sibling : undefined;
|
|
222
|
+
}
|
|
223
|
+
//# sourceMappingURL=private-dex-engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"private-dex-engine.js","sourceRoot":"","sources":["../../../src/testing/private-dex-engine.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ScriptedModelClient, scriptedModelApi, scriptedModelProvider, type ScriptedModelReply, type ScriptedModelResponder, type ScriptedToolCall, } from "../stepwise/scripted-model-client.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
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
|
+
// The scripted model client lives in the stepwise entry point; this path keeps the 0.1.0 module.
|
|
9
|
+
export { ScriptedModelClient, scriptedModelApi, scriptedModelProvider, } from "../stepwise/scripted-model-client.js";
|
|
10
|
+
//# sourceMappingURL=scripted-model-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scripted-model-client.js","sourceRoot":"","sources":["../../../src/testing/scripted-model-client.ts"],"names":[],"mappings":"AAAA,yCAAyC;AACzC,EAAE;AACF,kDAAkD;AAClD,mEAAmE;AACnE,+CAA+C;AAC/C,EAAE;AACF,0DAA0D;AAG1D,iGAAiG;AACjG,OAAO,EACL,mBAAmB,EACnB,gBAAgB,EAChB,qBAAqB,GAItB,MAAM,sCAAsC,CAAC"}
|