@aexhq/sdk 0.28.0 → 0.28.1
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.
|
@@ -9,6 +9,14 @@
|
|
|
9
9
|
* reconnects (no gap, no duplicate). It stops on a terminal event, on abort,
|
|
10
10
|
* or when the caller breaks the iterator.
|
|
11
11
|
*
|
|
12
|
+
* A silently half-open socket (no close/error, no frames) is the dangerous
|
|
13
|
+
* case: the read loop would block forever and MISS a terminal that was already
|
|
14
|
+
* persisted server-side. So the client sends a tiny keep-alive ping the
|
|
15
|
+
* coordinator auto-responds to (hibernation-safe, no DO wake) and runs an idle
|
|
16
|
+
* watchdog: if no frame arrives within {@link CoordinatorStreamOptions.idleTimeoutMs},
|
|
17
|
+
* the socket is treated as dead and reconnected — resume-from-cursor then
|
|
18
|
+
* replays the terminal.
|
|
19
|
+
*
|
|
12
20
|
* Filtering and projection are the client's concern (the wire carries the
|
|
13
21
|
* whole run): compose {@link filterStream} with the envelope guards, and
|
|
14
22
|
* {@link mapStream} with {@link toAGUI}, on top of this stream.
|
|
@@ -23,6 +31,8 @@ export interface WebSocketLike {
|
|
|
23
31
|
addEventListener(type: "open" | "message" | "close" | "error", listener: (ev: {
|
|
24
32
|
data?: unknown;
|
|
25
33
|
}) => void): void;
|
|
34
|
+
/** Send a keep-alive ping. Optional: a transport without it just relies on real events to reset the watchdog. */
|
|
35
|
+
send?(data: string): void;
|
|
26
36
|
}
|
|
27
37
|
export type WebSocketFactory = (url: string) => WebSocketLike;
|
|
28
38
|
export interface CoordinatorStreamOptions {
|
|
@@ -47,6 +57,20 @@ export interface CoordinatorStreamOptions {
|
|
|
47
57
|
* once a subsequent `getRun` is guaranteed terminal.
|
|
48
58
|
*/
|
|
49
59
|
readonly isTerminal?: (event: AexEvent) => boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Half-open watchdog window. If no frame (a real event OR a keep-alive pong)
|
|
62
|
+
* arrives within this many ms, the socket is treated as dead and reconnected
|
|
63
|
+
* (resume from cursor). Default 45s. Set 0 to disable.
|
|
64
|
+
*/
|
|
65
|
+
readonly idleTimeoutMs?: number;
|
|
66
|
+
/**
|
|
67
|
+
* Client keep-alive ping cadence. The client sends {@link COORDINATOR_PING},
|
|
68
|
+
* which the coordinator auto-responds to WITHOUT waking the durable object, so
|
|
69
|
+
* a legitimately quiet run keeps the socket measurably alive and does not trip
|
|
70
|
+
* the watchdog. Default 15s. Set 0 to disable (then only real events reset the
|
|
71
|
+
* watchdog → quiet runs may reconnect).
|
|
72
|
+
*/
|
|
73
|
+
readonly pingIntervalMs?: number;
|
|
50
74
|
}
|
|
51
75
|
export declare function streamCoordinatorEvents(opts: CoordinatorStreamOptions): AsyncGenerator<AexEvent, void, void>;
|
|
52
76
|
/** Async-iterable filter — keep only events matching the predicate. */
|
|
@@ -9,6 +9,14 @@
|
|
|
9
9
|
* reconnects (no gap, no duplicate). It stops on a terminal event, on abort,
|
|
10
10
|
* or when the caller breaks the iterator.
|
|
11
11
|
*
|
|
12
|
+
* A silently half-open socket (no close/error, no frames) is the dangerous
|
|
13
|
+
* case: the read loop would block forever and MISS a terminal that was already
|
|
14
|
+
* persisted server-side. So the client sends a tiny keep-alive ping the
|
|
15
|
+
* coordinator auto-responds to (hibernation-safe, no DO wake) and runs an idle
|
|
16
|
+
* watchdog: if no frame arrives within {@link CoordinatorStreamOptions.idleTimeoutMs},
|
|
17
|
+
* the socket is treated as dead and reconnected — resume-from-cursor then
|
|
18
|
+
* replays the terminal.
|
|
19
|
+
*
|
|
12
20
|
* Filtering and projection are the client's concern (the wire carries the
|
|
13
21
|
* whole run): compose {@link filterStream} with the envelope guards, and
|
|
14
22
|
* {@link mapStream} with {@link toAGUI}, on top of this stream.
|
|
@@ -17,11 +25,23 @@
|
|
|
17
25
|
* (Bun and Node 22+ ship it; no dependency) and tests drive a fake.
|
|
18
26
|
*/
|
|
19
27
|
const isTerminalType = (e) => e.type === "RUN_FINISHED" || e.type === "RUN_ERROR";
|
|
28
|
+
/**
|
|
29
|
+
* Keep-alive ping the client sends; the coordinator answers it via
|
|
30
|
+
* `setWebSocketAutoResponse` without waking the DO. Must stay byte-identical to
|
|
31
|
+
* the coordinator's pair (aex-platform `packages/shared/src/event-stream-client.ts`).
|
|
32
|
+
*/
|
|
33
|
+
const COORDINATOR_PING = "aex:ping";
|
|
34
|
+
/** Default half-open watchdog window — 3× the ping cadence, so 2 pongs can be lost. */
|
|
35
|
+
const DEFAULT_IDLE_TIMEOUT_MS = 45_000;
|
|
36
|
+
/** Default client keep-alive ping cadence. */
|
|
37
|
+
const DEFAULT_PING_INTERVAL_MS = 15_000;
|
|
20
38
|
export async function* streamCoordinatorEvents(opts) {
|
|
21
39
|
const makeWs = opts.webSocketFactory ?? ((url) => new WebSocket(url));
|
|
22
40
|
const isTerminal = opts.isTerminal ?? isTerminalType;
|
|
23
41
|
const reconnectDelayMs = opts.reconnectDelayMs ?? 500;
|
|
24
42
|
const maxReconnects = opts.maxReconnects ?? Number.POSITIVE_INFINITY;
|
|
43
|
+
const idleTimeoutMs = opts.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
|
|
44
|
+
const pingIntervalMs = opts.pingIntervalMs ?? DEFAULT_PING_INTERVAL_MS;
|
|
25
45
|
let cursor = (opts.from ?? 0) - 1;
|
|
26
46
|
let attempts = 0;
|
|
27
47
|
let done = false;
|
|
@@ -42,7 +62,50 @@ export async function* streamCoordinatorEvents(opts) {
|
|
|
42
62
|
r();
|
|
43
63
|
}
|
|
44
64
|
};
|
|
65
|
+
let idleTimer = null;
|
|
66
|
+
let pingTimer = null;
|
|
67
|
+
const stopTimers = () => {
|
|
68
|
+
if (idleTimer !== null) {
|
|
69
|
+
clearTimeout(idleTimer);
|
|
70
|
+
idleTimer = null;
|
|
71
|
+
}
|
|
72
|
+
if (pingTimer !== null) {
|
|
73
|
+
clearInterval(pingTimer);
|
|
74
|
+
pingTimer = null;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
// Re-arm on every inbound frame. On expiry the socket is presumed half-open
|
|
78
|
+
// → close it and let the loop fall through to reconnect (resume from cursor).
|
|
79
|
+
const armIdle = () => {
|
|
80
|
+
if (idleTimeoutMs <= 0)
|
|
81
|
+
return;
|
|
82
|
+
if (idleTimer !== null)
|
|
83
|
+
clearTimeout(idleTimer);
|
|
84
|
+
idleTimer = setTimeout(() => {
|
|
85
|
+
idleTimer = null;
|
|
86
|
+
if (closed)
|
|
87
|
+
return;
|
|
88
|
+
closed = true;
|
|
89
|
+
disconnectReason = "idle";
|
|
90
|
+
closeQuietly(ws);
|
|
91
|
+
wake();
|
|
92
|
+
}, idleTimeoutMs);
|
|
93
|
+
};
|
|
94
|
+
ws.addEventListener("open", () => {
|
|
95
|
+
armIdle();
|
|
96
|
+
if (pingIntervalMs > 0 && typeof ws.send === "function") {
|
|
97
|
+
pingTimer = setInterval(() => {
|
|
98
|
+
try {
|
|
99
|
+
ws.send(COORDINATOR_PING);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// socket not open / send unsupported — the idle watchdog still covers it
|
|
103
|
+
}
|
|
104
|
+
}, pingIntervalMs);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
45
107
|
ws.addEventListener("message", (ev) => {
|
|
108
|
+
armIdle();
|
|
46
109
|
const data = typeof ev.data === "string" ? ev.data : "";
|
|
47
110
|
if (!data)
|
|
48
111
|
return;
|
|
@@ -58,22 +121,33 @@ export async function* streamCoordinatorEvents(opts) {
|
|
|
58
121
|
}
|
|
59
122
|
});
|
|
60
123
|
ws.addEventListener("close", (ev) => {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
124
|
+
stopTimers();
|
|
125
|
+
// Don't clobber a reason already decided (idle/abort closes the socket too).
|
|
126
|
+
if (!closed) {
|
|
127
|
+
closed = true;
|
|
128
|
+
const code = ev?.code;
|
|
129
|
+
disconnectReason = `close${typeof code === "number" ? ` code=${code}` : ""}`;
|
|
130
|
+
}
|
|
64
131
|
wake();
|
|
65
132
|
});
|
|
66
133
|
ws.addEventListener("error", () => {
|
|
67
|
-
|
|
68
|
-
|
|
134
|
+
stopTimers();
|
|
135
|
+
if (!closed) {
|
|
136
|
+
closed = true;
|
|
137
|
+
disconnectReason = "error";
|
|
138
|
+
}
|
|
69
139
|
wake();
|
|
70
140
|
});
|
|
71
141
|
const onAbort = () => {
|
|
72
142
|
closeQuietly(ws);
|
|
73
143
|
closed = true;
|
|
144
|
+
stopTimers();
|
|
74
145
|
wake();
|
|
75
146
|
};
|
|
76
147
|
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
148
|
+
// Arm immediately: a connect that never reaches "open" (and fakes that
|
|
149
|
+
// never emit it) must still time out rather than hang forever.
|
|
150
|
+
armIdle();
|
|
77
151
|
try {
|
|
78
152
|
while (true) {
|
|
79
153
|
while (queue.length > 0) {
|
|
@@ -95,6 +169,7 @@ export async function* streamCoordinatorEvents(opts) {
|
|
|
95
169
|
}
|
|
96
170
|
}
|
|
97
171
|
finally {
|
|
172
|
+
stopTimers();
|
|
98
173
|
opts.signal?.removeEventListener("abort", onAbort);
|
|
99
174
|
}
|
|
100
175
|
if (done || opts.signal?.aborted)
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aexhq/sdk",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.1",
|
|
4
4
|
"description": "TypeScript SDK for running autonomous agent sessions across providers (Anthropic, OpenAI, DeepSeek, Gemini, Mistral) behind one interface.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|