@dungle-scrubs/tether-client 0.1.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.
@@ -0,0 +1,443 @@
1
+ import { type BoundaryDebugInfo } from "./observability.js";
2
+ import { type AppendSessionEventInput } from "./protocol.js";
3
+ import { type TaskCancellationContext } from "./participant-claimable-task-runner.js";
4
+ import type { ParticipantRuntimeKind, SessionEvent, TaskRecord } from "./types.js";
5
+ /**
6
+ * Error raised when participant runtime client configuration is invalid.
7
+ */
8
+ export declare class ParticipantRuntimeClientConfigurationError extends Error {
9
+ /** Machine-readable configuration field that failed validation. */
10
+ readonly field: string;
11
+ constructor(field: string, message: string);
12
+ }
13
+ /**
14
+ * Error raised when the server returns a correlated command error envelope.
15
+ */
16
+ export declare class ParticipantRuntimeCommandError extends Error {
17
+ /** WebSocket command operation that rejected. */
18
+ readonly op: string;
19
+ /** Number of pending commands observed when the rejection was handled. */
20
+ readonly pendingCommandCount: number;
21
+ /** Correlation id attached to the command request. */
22
+ readonly requestId: string;
23
+ /** Task id carried by task-scoped commands when present. */
24
+ readonly taskId?: string;
25
+ constructor(input: {
26
+ readonly message: string;
27
+ readonly op: string;
28
+ readonly pendingCommandCount: number;
29
+ readonly requestId: string;
30
+ readonly taskId?: string;
31
+ });
32
+ }
33
+ /**
34
+ * Error raised when a correlated WebSocket command does not receive a response
35
+ * before the configured command timeout.
36
+ */
37
+ export declare class ParticipantRuntimeCommandTimeoutError extends Error {
38
+ /** WebSocket command operation that timed out. */
39
+ readonly op: string;
40
+ /** Number of pending commands observed when the timeout fired. */
41
+ readonly pendingCommandCount: number;
42
+ /** Correlation id attached to the command request. */
43
+ readonly requestId: string;
44
+ /** Task id carried by task-scoped commands when present. */
45
+ readonly taskId?: string;
46
+ /** Command response timeout in milliseconds. */
47
+ readonly timeoutMs: number;
48
+ constructor(input: {
49
+ readonly op: string;
50
+ readonly pendingCommandCount: number;
51
+ readonly requestId: string;
52
+ readonly taskId?: string;
53
+ readonly timeoutMs: number;
54
+ });
55
+ }
56
+ /**
57
+ * Result returned by an adapter-specific task executor after task work finishes.
58
+ */
59
+ export interface ParticipantTaskExecutorResult {
60
+ /** Optional user-visible output event to publish before completion. */
61
+ readonly output?: string | null;
62
+ /** Structured completion payload stored on the durable task row. */
63
+ readonly result: Record<string, unknown>;
64
+ }
65
+ /**
66
+ * Runtime helpers and claimed task state passed to adapter-specific executors.
67
+ */
68
+ export interface ParticipantTaskExecutorContext {
69
+ /** Concrete runtime process id that owns the task claim. */
70
+ readonly instanceId: string;
71
+ /** Stable participant identity that owns the task claim. */
72
+ readonly participantId: string;
73
+ /** Bounded session events observed before this task executor started. */
74
+ readonly recentEvents: readonly SessionEvent[];
75
+ /** Publishes an additional user-visible output event for the active task. */
76
+ readonly publishOutput: (output: string) => Promise<void>;
77
+ /** Publishes an additional progress event for the active task. */
78
+ readonly publishProgress: () => Promise<void>;
79
+ /** Cancellation signal aborted when Tether cancels the active task or its claim is lost. */
80
+ readonly signal: AbortSignal;
81
+ /** Durable session that owns the task. */
82
+ readonly sessionId: string;
83
+ /** Claimed task record returned by Tether after this participant won the claim. */
84
+ readonly task: TaskRecord;
85
+ }
86
+ /**
87
+ * Executes a claimed task and returns the durable completion payload.
88
+ */
89
+ export type ParticipantTaskExecutor = (context: ParticipantTaskExecutorContext) => Promise<ParticipantTaskExecutorResult>;
90
+ /**
91
+ * Decides whether a participant runtime should attempt a claim for a task.
92
+ */
93
+ export type ParticipantTaskSelector = (task: TaskRecord) => boolean;
94
+ /**
95
+ * Durable resume cursor for a participant runtime. When supplied, the runtime
96
+ * resumes from the persisted sequence instead of replaying a fixed window from
97
+ * `afterSeq` on every process start. `read` runs once at startup; `write` runs
98
+ * throttled as events are handled and once more on graceful close.
99
+ */
100
+ export interface ParticipantRuntimeCursorStore {
101
+ /** Returns the last persisted event sequence, or null when none is stored. */
102
+ read(): number | null | Promise<number | null>;
103
+ /** Persists the highest event sequence the runtime has confirmed as handled. */
104
+ write(seq: number): void | Promise<void>;
105
+ }
106
+ /**
107
+ * Throttle policy for durable cursor writes. Persists at most once per
108
+ * `intervalMs`, or immediately once `eventCount` events have been observed
109
+ * since the last write.
110
+ */
111
+ export interface ParticipantRuntimeCursorPersistPolicy {
112
+ /** Maximum observed events between writes before forcing a persist. */
113
+ readonly eventCount?: number;
114
+ /** Minimum delay between throttled cursor writes in milliseconds. */
115
+ readonly intervalMs?: number;
116
+ }
117
+ /**
118
+ * Connection settings for one external participant runtime process.
119
+ */
120
+ export interface ParticipantRuntimeClientConfig {
121
+ /** Last observed event sequence to resume after; acts as a resume floor. */
122
+ readonly afterSeq: number;
123
+ /** Bearer token sent to Tether; falls back to SERVICE_AUTH_TOKEN/TETHER_AUTH_TOKEN when omitted. */
124
+ readonly authToken?: string | null;
125
+ /** Public participant capabilities registered with the session. */
126
+ readonly capabilities: Record<string, unknown>;
127
+ /** Maximum time to wait for one correlated WebSocket command response. */
128
+ readonly commandTimeoutMs?: number;
129
+ /** Optional throttle policy for durable cursor writes. */
130
+ readonly cursorPersist?: ParticipantRuntimeCursorPersistPolicy;
131
+ /** Optional durable resume cursor. When omitted, resume behavior is unchanged. */
132
+ readonly cursorStore?: ParticipantRuntimeCursorStore;
133
+ /** Display name shown in participant presence. */
134
+ readonly displayName: string;
135
+ /** Concrete runtime process id that owns the WebSocket control lease. */
136
+ readonly instanceId: string;
137
+ /** Stable or generated participant identity controlled by this runtime. */
138
+ readonly participantId: string;
139
+ /** Optional reconnect backoff settings for long-running adapters. */
140
+ readonly reconnect?: {
141
+ /** Initial delay before reconnecting after an unexpected disconnect. */
142
+ readonly baseDelayMs?: number;
143
+ /** Maximum reconnect delay after repeated failures. */
144
+ readonly maxDelayMs?: number;
145
+ };
146
+ /** Runtime implementation kind, such as `generic_agent` or `codex`. */
147
+ readonly runtimeKind: ParticipantRuntimeKind;
148
+ /** Base URL of the Tether HTTP service. */
149
+ readonly serviceUrl: string;
150
+ /** Session the participant runtime connects to. */
151
+ readonly sessionId: string;
152
+ }
153
+ /**
154
+ * Options for processing claimable tasks from the event stream.
155
+ */
156
+ export interface ParticipantRuntimeTaskLoopOptions {
157
+ /** Interval used to refresh active task claim leases. */
158
+ readonly claimRefreshMs: number;
159
+ /** Adapter-specific task executor. */
160
+ readonly executor: ParticipantTaskExecutor;
161
+ /** Whether to process replayed claimable work once and exit. */
162
+ readonly once: boolean;
163
+ /** Adapter-specific task selection rule. */
164
+ readonly shouldClaimTask: ParticipantTaskSelector;
165
+ }
166
+ /** Cleanup callback returned by participant runtime lifecycle hooks. */
167
+ export type ParticipantRuntimeCleanup = () => void | Promise<void>;
168
+ /** Optional lifecycle hooks for the high-level participant runtime runner. */
169
+ export interface RunParticipantRuntimeHooks {
170
+ /**
171
+ * Handles stream and reconnect errors. Omit to use structured stderr
172
+ * logging; pass null only when the embedding host installs an equivalent
173
+ * visible error path.
174
+ */
175
+ readonly onError?: ((error: Error) => void) | null;
176
+ /**
177
+ * Runs after historical replay completes and before the task claim loop
178
+ * starts. Use this to publish startup status or register observers that must
179
+ * receive replay backlog. A returned cleanup callback runs after the claim
180
+ * loop exits.
181
+ */
182
+ readonly onReplayComplete?: (client: ParticipantRuntimeClient) => undefined | ParticipantRuntimeCleanup | Promise<undefined | ParticipantRuntimeCleanup>;
183
+ }
184
+ /**
185
+ * High-level adapter entry point for running one participant runtime.
186
+ */
187
+ export interface RunParticipantRuntimeInput extends ParticipantRuntimeClientConfig {
188
+ /** Interval used to refresh active task claim leases. */
189
+ readonly claimRefreshMs: number;
190
+ /** Adapter-specific task executor. */
191
+ readonly executor: ParticipantTaskExecutor;
192
+ /** Optional lifecycle and error hooks for adapters with replay-ready setup. */
193
+ readonly hooks?: RunParticipantRuntimeHooks;
194
+ /** Whether to process replayed claimable work once and exit. */
195
+ readonly once?: boolean;
196
+ /** Optional adapter-specific selector; defaults to work kind and participant ownership checks. */
197
+ readonly shouldClaimTask?: ParticipantTaskSelector;
198
+ /** Task kinds this runtime is willing to claim. */
199
+ readonly workKinds: readonly string[];
200
+ }
201
+ /**
202
+ * Runtime diagnostics for one participant WebSocket client.
203
+ */
204
+ export interface ParticipantRuntimeClientDebugInfo extends BoundaryDebugInfo {
205
+ /** Number of events received before a handler was attached. */
206
+ readonly eventBacklogSize: number;
207
+ /** Number of registered event handlers. */
208
+ readonly eventHandlerCount: number;
209
+ /** Highest event sequence observed by this client. */
210
+ readonly lastObservedSeq: number;
211
+ /** Number of failed reconnect attempts since startup. */
212
+ readonly reconnectFailureCount: number;
213
+ /** Number of successful reconnects since startup. */
214
+ readonly reconnectSuccessCount: number;
215
+ /** Whether the client has been intentionally closed. */
216
+ readonly stopped: boolean;
217
+ /** Number of in-flight command requests waiting for server responses. */
218
+ readonly pendingCommandCount: number;
219
+ /** Underlying WebSocket ready state. */
220
+ readonly socketReadyState: number;
221
+ }
222
+ /**
223
+ * Runs a participant runtime using the reusable client boundary. Adapters call
224
+ * this when they only need to provide identity, capabilities, task selection,
225
+ * and execution policy.
226
+ */
227
+ export declare function runParticipantRuntime(input: RunParticipantRuntimeInput): Promise<void>;
228
+ /**
229
+ * Checks whether a task is claimable by a participant with a work-kind allow
230
+ * list.
231
+ */
232
+ export declare function shouldClaimParticipantTask(task: TaskRecord, workKinds: readonly string[], participantId: string): boolean;
233
+ /**
234
+ * Reusable WebSocket runtime client for external participants. It owns replay
235
+ * buffering, command correlation, task claiming, claim refresh, cancellation,
236
+ * and shutdown hooks so adapter workers only provide task-selection and
237
+ * execution policy.
238
+ */
239
+ export declare class ParticipantRuntimeClient {
240
+ private readonly config;
241
+ private static readonly maxRecentEvents;
242
+ private readonly closeHandlers;
243
+ private closePromise;
244
+ private readonly eventBacklog;
245
+ private readonly eventHandlers;
246
+ private readonly errorHandlers;
247
+ private lastObservedSeq;
248
+ private lastHandledSeq;
249
+ private lastPersistedSeq;
250
+ private eventsSinceCursorPersist;
251
+ private cursorPersistTimer;
252
+ private readonly cursorPersistIntervalMs;
253
+ private readonly cursorPersistEventCount;
254
+ private readonly commandTimeoutMs;
255
+ private readonly observability;
256
+ private readonly pendingCommands;
257
+ private readonly recentEvents;
258
+ private reconnectFailureCount;
259
+ private reconnectSuccessCount;
260
+ private replayComplete;
261
+ private rejectReplayComplete;
262
+ private resolveReplayComplete;
263
+ private replayCompleteSettled;
264
+ private socket;
265
+ private stopped;
266
+ /**
267
+ * Attaches protocol message handling to an already-created WebSocket.
268
+ */
269
+ private constructor();
270
+ /**
271
+ * Opens the WebSocket stream and returns a command-capable participant client.
272
+ * When a durable cursor is configured, the stream resumes from the higher of
273
+ * `afterSeq` and the persisted cursor so restarts replay only actual downtime.
274
+ */
275
+ static connect(config: ParticipantRuntimeClientConfig): Promise<ParticipantRuntimeClient>;
276
+ /**
277
+ * Publishes one participant-originated event over the WebSocket command
278
+ * channel.
279
+ */
280
+ appendEvent(input: AppendSessionEventInput): Promise<void>;
281
+ /**
282
+ * Attempts to claim a task and returns null when another participant won.
283
+ */
284
+ claimTask(taskId: string): Promise<TaskRecord | null>;
285
+ /**
286
+ * Requests a graceful WebSocket close.
287
+ */
288
+ close(): void;
289
+ /**
290
+ * Drops the current transport without stopping the client. Long-running task
291
+ * loops reconnect from the last observed event sequence.
292
+ */
293
+ disconnect(): void;
294
+ /**
295
+ * Returns client-side queue, cursor, and pending-command counters.
296
+ */
297
+ debugInfo(): ParticipantRuntimeClientDebugInfo;
298
+ /**
299
+ * Completes the active task claim with a structured result payload.
300
+ */
301
+ completeTask(taskId: string, result: Record<string, unknown>): Promise<void>;
302
+ /**
303
+ * Fails the active task claim with a structured failure payload.
304
+ */
305
+ failTask(taskId: string, failure: Record<string, unknown>): Promise<void>;
306
+ /**
307
+ * Registers a close callback and returns an unsubscribe function.
308
+ */
309
+ onClose(handler: () => void): () => void;
310
+ /**
311
+ * Registers an error callback and returns an unsubscribe function.
312
+ */
313
+ onError(handler: (error: Error) => void): () => void;
314
+ /**
315
+ * Registers an event callback and drains any events received before the
316
+ * handler was attached.
317
+ */
318
+ onEvent(handler: (event: SessionEvent) => void): () => void;
319
+ /**
320
+ * Reopens the participant stream from the highest observed event sequence.
321
+ */
322
+ reconnect(): Promise<ParticipantRuntimeClient>;
323
+ /**
324
+ * Refreshes the active task claim lease and returns null if the claim is no
325
+ * longer valid.
326
+ */
327
+ refreshTaskClaim(taskId: string): Promise<TaskRecord | null>;
328
+ /**
329
+ * Processes claimable tasks from replay and live events until either the
330
+ * one-shot replay work completes or the socket closes.
331
+ */
332
+ runClaimableTasks(options: ParticipantRuntimeTaskLoopOptions): Promise<void>;
333
+ /**
334
+ * Runs the claim, progress, execution, output, and completion sequence for
335
+ * one task while respecting cancellation.
336
+ */
337
+ runTaskClaimFlow(input: {
338
+ readonly cancellation: TaskCancellationContext;
339
+ readonly claimRefreshMs: number;
340
+ readonly executor: ParticipantTaskExecutor;
341
+ readonly task: TaskRecord;
342
+ }): Promise<void>;
343
+ /**
344
+ * Resolves when the current socket closes.
345
+ */
346
+ waitForClose(): Promise<void>;
347
+ /**
348
+ * Resolves once the server has finished replaying historical events.
349
+ */
350
+ waitForReplayComplete(): Promise<void>;
351
+ /**
352
+ * Opens a WebSocket stream from the requested event cursor.
353
+ */
354
+ private open;
355
+ /**
356
+ * Reopens the WebSocket stream with bounded retry backoff.
357
+ */
358
+ private reconnectWithBackoff;
359
+ /**
360
+ * Builds the bounded reconnect loop as an Effect program.
361
+ */
362
+ private buildReconnectWithBackoff;
363
+ /**
364
+ * Returns the currently open socket or throws a connection-level error.
365
+ */
366
+ private requireOpenSocket;
367
+ /**
368
+ * Parses one server envelope and routes it to event handlers, pending command
369
+ * promises, or replay completion state.
370
+ */
371
+ private handleMessage;
372
+ /**
373
+ * Routes a connection-level or message-handling error to registered error
374
+ * handlers instead of throwing out of a ws event listener, which would
375
+ * surface as an unhandled exception.
376
+ */
377
+ private emitError;
378
+ /** Resolves the replay wait once, preserving later reconnect state. */
379
+ private settleReplayComplete;
380
+ /** Rejects the replay wait once when the socket fails before replay.complete. */
381
+ private settleReplayCompleteError;
382
+ /**
383
+ * Keeps a bounded in-memory window of replay/live events for executors that
384
+ * need conversation context without reimplementing stream replay.
385
+ */
386
+ private rememberEvent;
387
+ /**
388
+ * Resolves the startup resume sequence from the durable cursor, never below
389
+ * the configured `afterSeq` floor. Returns `afterSeq` when no cursor store is
390
+ * configured, preserving the fixed-window resume behavior.
391
+ */
392
+ private resolveInitialResumeSeq;
393
+ /**
394
+ * Advances the handled cursor once an event's handlers have been invoked and
395
+ * schedules a durable persist. Only sequences that have actually been
396
+ * delivered to a handler reach the durable store, so a restart never resumes
397
+ * past an event that was merely received but never handled. No-op when the
398
+ * sequence has not advanced past the last handled value.
399
+ */
400
+ private markSeqHandled;
401
+ /**
402
+ * Records that the handled cursor advanced and persists it under the
403
+ * configured throttle: immediately once enough events accumulate, otherwise
404
+ * on a trailing timer. No-op when no cursor store is configured.
405
+ */
406
+ private scheduleCursorPersist;
407
+ /**
408
+ * Writes the highest handled event sequence to the durable cursor store,
409
+ * skipping when it has not advanced past the last persisted value. Only
410
+ * sequences confirmed as handled are persisted, never a value ahead of the
411
+ * events the runtime has actually processed.
412
+ */
413
+ private persistCursor;
414
+ /**
415
+ * Rejects all pending command promises with a shared connection-level error.
416
+ */
417
+ private rejectPendingCommands;
418
+ /**
419
+ * Sends a command with a generated request id and waits for its matching
420
+ * command result or error envelope.
421
+ */
422
+ private sendCommand;
423
+ /**
424
+ * Builds one correlated WebSocket command request and guarantees pending
425
+ * command cleanup if the waiting Effect is interrupted.
426
+ */
427
+ private buildCommandRequest;
428
+ /** Removes one pending command and clears its timer before settlement. */
429
+ private settlePendingCommand;
430
+ }
431
+ /**
432
+ * Builds the WebSocket stream URL with participant identity and capabilities in
433
+ * query parameters.
434
+ */
435
+ export declare function buildParticipantRuntimeStreamUrl(config: ParticipantRuntimeClientConfig): string;
436
+ /**
437
+ * Resolves the durable resume sequence as the higher of the configured
438
+ * `afterSeq` floor and the persisted cursor, treating a missing or non-finite
439
+ * stored value as zero. The resume point is never below `afterSeq`.
440
+ */
441
+ export declare function resolveResumeSeq(afterSeq: number, storedSeq: number | null): number;
442
+ /** Resolves and validates the finite command response timeout. */
443
+ export declare function resolveCommandTimeoutMs(commandTimeoutMs: number | undefined): number;