@ably/ai-transport 0.0.1 → 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/README.md +114 -116
- package/dist/ably-ai-transport.js +1743 -961
- package/dist/ably-ai-transport.js.map +1 -1
- package/dist/ably-ai-transport.umd.cjs +1 -1
- package/dist/ably-ai-transport.umd.cjs.map +1 -1
- package/dist/constants.d.ts +117 -39
- package/dist/core/agent.d.ts +29 -0
- package/dist/core/codec/decoder.d.ts +20 -23
- package/dist/core/codec/encoder.d.ts +11 -8
- package/dist/core/codec/index.d.ts +1 -2
- package/dist/core/codec/lifecycle-tracker.d.ts +10 -9
- package/dist/core/codec/types.d.ts +410 -101
- package/dist/core/transport/agent-session.d.ts +10 -0
- package/dist/core/transport/branch-chain.d.ts +43 -0
- package/dist/core/transport/client-session.d.ts +13 -0
- package/dist/core/transport/decode-fold.d.ts +47 -0
- package/dist/core/transport/headers.d.ts +97 -17
- package/dist/core/transport/index.d.ts +5 -3
- package/dist/core/transport/internal/bounded-map.d.ts +20 -0
- package/dist/core/transport/invocation.d.ts +74 -0
- package/dist/core/transport/load-conversation.d.ts +128 -0
- package/dist/core/transport/load-history.d.ts +39 -0
- package/dist/core/transport/pipe-stream.d.ts +9 -8
- package/dist/core/transport/run-manager.d.ts +78 -0
- package/dist/core/transport/tree.d.ts +435 -0
- package/dist/core/transport/types/agent.d.ts +353 -0
- package/dist/core/transport/types/client.d.ts +168 -0
- package/dist/core/transport/types/shared.d.ts +24 -0
- package/dist/core/transport/types/tree.d.ts +315 -0
- package/dist/core/transport/types/view.d.ts +222 -0
- package/dist/core/transport/types.d.ts +13 -402
- package/dist/core/transport/view.d.ts +354 -0
- package/dist/errors.d.ts +37 -9
- package/dist/index.d.ts +6 -6
- package/dist/logger.d.ts +12 -0
- package/dist/react/ably-ai-transport-react.js +1164 -645
- package/dist/react/ably-ai-transport-react.js.map +1 -1
- package/dist/react/ably-ai-transport-react.umd.cjs +1 -1
- package/dist/react/ably-ai-transport-react.umd.cjs.map +1 -1
- package/dist/react/contexts/client-session-context.d.ts +36 -0
- package/dist/react/contexts/client-session-provider.d.ts +53 -0
- package/dist/react/create-session-hooks.d.ts +116 -0
- package/dist/react/index.d.ts +16 -10
- package/dist/react/internal/use-resolved-session.d.ts +36 -0
- package/dist/react/use-ably-messages.d.ts +20 -11
- package/dist/react/use-client-session.d.ts +81 -0
- package/dist/react/use-create-view.d.ts +23 -0
- package/dist/react/use-tree.d.ts +35 -0
- package/dist/react/use-view.d.ts +110 -0
- package/dist/utils.d.ts +32 -23
- package/dist/vercel/ably-ai-transport-vercel.js +2748 -1625
- package/dist/vercel/ably-ai-transport-vercel.js.map +1 -1
- package/dist/vercel/ably-ai-transport-vercel.umd.cjs +1 -1
- package/dist/vercel/ably-ai-transport-vercel.umd.cjs.map +1 -1
- package/dist/vercel/codec/decoder.d.ts +5 -18
- package/dist/vercel/codec/encoder.d.ts +6 -36
- package/dist/vercel/codec/events.d.ts +51 -0
- package/dist/vercel/codec/index.d.ts +24 -12
- package/dist/vercel/codec/reducer.d.ts +144 -0
- package/dist/vercel/codec/tool-transitions.d.ts +50 -0
- package/dist/vercel/index.d.ts +4 -2
- package/dist/vercel/react/ably-ai-transport-vercel-react.js +10298 -1410
- package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +70 -1
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
- package/dist/vercel/react/contexts/chat-transport-context.d.ts +33 -0
- package/dist/vercel/react/contexts/chat-transport-provider.d.ts +96 -0
- package/dist/vercel/react/index.d.ts +4 -0
- package/dist/vercel/react/use-chat-transport.d.ts +66 -21
- package/dist/vercel/react/use-message-sync.d.ts +31 -12
- package/dist/vercel/run-end-reason.d.ts +29 -0
- package/dist/vercel/transport/chat-transport.d.ts +71 -30
- package/dist/vercel/transport/index.d.ts +25 -18
- package/dist/vercel/transport/run-output-stream.d.ts +56 -0
- package/dist/version.d.ts +2 -0
- package/package.json +47 -34
- package/src/constants.ts +126 -47
- package/src/core/agent.ts +68 -0
- package/src/core/codec/decoder.ts +71 -98
- package/src/core/codec/encoder.ts +115 -58
- package/src/core/codec/index.ts +13 -6
- package/src/core/codec/lifecycle-tracker.ts +10 -9
- package/src/core/codec/types.ts +438 -106
- package/src/core/transport/agent-session.ts +1344 -0
- package/src/core/transport/branch-chain.ts +58 -0
- package/src/core/transport/client-session.ts +775 -0
- package/src/core/transport/decode-fold.ts +91 -0
- package/src/core/transport/headers.ts +182 -19
- package/src/core/transport/index.ts +29 -22
- package/src/core/transport/internal/bounded-map.ts +27 -0
- package/src/core/transport/invocation.ts +98 -0
- package/src/core/transport/load-conversation.ts +355 -0
- package/src/core/transport/load-history.ts +269 -0
- package/src/core/transport/pipe-stream.ts +58 -40
- package/src/core/transport/run-manager.ts +249 -0
- package/src/core/transport/tree.ts +1167 -0
- package/src/core/transport/types/agent.ts +407 -0
- package/src/core/transport/types/client.ts +211 -0
- package/src/core/transport/types/shared.ts +27 -0
- package/src/core/transport/types/tree.ts +344 -0
- package/src/core/transport/types/view.ts +259 -0
- package/src/core/transport/types.ts +13 -527
- package/src/core/transport/view.ts +1271 -0
- package/src/errors.ts +42 -9
- package/src/event-emitter.ts +3 -2
- package/src/index.ts +55 -39
- package/src/logger.ts +14 -1
- package/src/react/contexts/client-session-context.ts +41 -0
- package/src/react/contexts/client-session-provider.tsx +186 -0
- package/src/react/create-session-hooks.ts +141 -0
- package/src/react/index.ts +27 -10
- package/src/react/internal/use-resolved-session.ts +63 -0
- package/src/react/use-ably-messages.ts +47 -19
- package/src/react/use-client-session.ts +201 -0
- package/src/react/use-create-view.ts +72 -0
- package/src/react/use-tree.ts +84 -0
- package/src/react/use-view.ts +275 -0
- package/src/react/vite.config.ts +4 -1
- package/src/utils.ts +63 -45
- package/src/vercel/codec/decoder.ts +336 -255
- package/src/vercel/codec/encoder.ts +348 -196
- package/src/vercel/codec/events.ts +87 -0
- package/src/vercel/codec/index.ts +59 -14
- package/src/vercel/codec/reducer.ts +977 -0
- package/src/vercel/codec/tool-transitions.ts +122 -0
- package/src/vercel/index.ts +7 -3
- package/src/vercel/react/contexts/chat-transport-context.ts +41 -0
- package/src/vercel/react/contexts/chat-transport-provider.tsx +150 -0
- package/src/vercel/react/index.ts +13 -1
- package/src/vercel/react/use-chat-transport.ts +162 -42
- package/src/vercel/react/use-message-sync.ts +121 -22
- package/src/vercel/react/vite.config.ts +4 -2
- package/src/vercel/run-end-reason.ts +78 -0
- package/src/vercel/transport/chat-transport.ts +553 -113
- package/src/vercel/transport/index.ts +40 -28
- package/src/vercel/transport/run-output-stream.ts +170 -0
- package/src/version.ts +2 -0
- package/dist/core/transport/client-transport.d.ts +0 -10
- package/dist/core/transport/conversation-tree.d.ts +0 -9
- package/dist/core/transport/decode-history.d.ts +0 -41
- package/dist/core/transport/server-transport.d.ts +0 -7
- package/dist/core/transport/stream-router.d.ts +0 -19
- package/dist/core/transport/turn-manager.d.ts +0 -34
- package/dist/react/use-active-turns.d.ts +0 -8
- package/dist/react/use-client-transport.d.ts +0 -7
- package/dist/react/use-conversation-tree.d.ts +0 -20
- package/dist/react/use-edit.d.ts +0 -7
- package/dist/react/use-history.d.ts +0 -19
- package/dist/react/use-messages.d.ts +0 -7
- package/dist/react/use-regenerate.d.ts +0 -7
- package/dist/react/use-send.d.ts +0 -7
- package/dist/vercel/codec/accumulator.d.ts +0 -21
- package/src/core/transport/client-transport.ts +0 -959
- package/src/core/transport/conversation-tree.ts +0 -434
- package/src/core/transport/decode-history.ts +0 -337
- package/src/core/transport/server-transport.ts +0 -458
- package/src/core/transport/stream-router.ts +0 -118
- package/src/core/transport/turn-manager.ts +0 -147
- package/src/react/use-active-turns.ts +0 -61
- package/src/react/use-client-transport.ts +0 -37
- package/src/react/use-conversation-tree.ts +0 -71
- package/src/react/use-edit.ts +0 -24
- package/src/react/use-history.ts +0 -111
- package/src/react/use-messages.ts +0 -32
- package/src/react/use-regenerate.ts +0 -24
- package/src/react/use-send.ts +0 -25
- package/src/vercel/codec/accumulator.ts +0 -603
|
@@ -1,68 +1,86 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure stream piping function.
|
|
3
3
|
*
|
|
4
|
-
* Reads
|
|
5
|
-
* and handles
|
|
4
|
+
* Reads outputs from a ReadableStream, writes them to an encoder via
|
|
5
|
+
* `publishOutput`, and handles cancel/error. No dependencies on run
|
|
6
|
+
* state or session internals.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import type { Logger } from '../../logger.js';
|
|
9
|
-
import type {
|
|
10
|
+
import type { CodecInputEvent, CodecOutputEvent, Encoder, WriteOptions } from '../codec/types.js';
|
|
10
11
|
import type { StreamResult } from './types.js';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
|
-
*
|
|
14
|
+
* Adapt an AbortSignal into a promise that resolves once the signal aborts,
|
|
15
|
+
* paired with a cleanup that detaches the listener. With no signal the promise
|
|
16
|
+
* never resolves (there is no cancellation path); an already-aborted signal
|
|
17
|
+
* resolves immediately. `cleanup` is a no-op unless a listener was attached.
|
|
18
|
+
* @param signal - The AbortSignal to watch, or undefined for no cancellation.
|
|
19
|
+
* @returns The abort promise and a cleanup to call when racing is done.
|
|
20
|
+
*/
|
|
21
|
+
const abortSignalToPromise = (signal: AbortSignal | undefined): { promise: Promise<void>; cleanup: () => void } => {
|
|
22
|
+
let listener: (() => void) | undefined;
|
|
23
|
+
const promise =
|
|
24
|
+
signal === undefined
|
|
25
|
+
? // eslint-disable-next-line @typescript-eslint/no-empty-function -- never-resolving promise: no signal means no cancellation path
|
|
26
|
+
new Promise<void>(() => {})
|
|
27
|
+
: signal.aborted
|
|
28
|
+
? Promise.resolve()
|
|
29
|
+
: new Promise<void>((resolve) => {
|
|
30
|
+
listener = () => {
|
|
31
|
+
resolve();
|
|
32
|
+
};
|
|
33
|
+
signal.addEventListener('abort', listener, { once: true });
|
|
34
|
+
});
|
|
35
|
+
const cleanup = (): void => {
|
|
36
|
+
if (listener && signal) signal.removeEventListener('abort', listener);
|
|
37
|
+
};
|
|
38
|
+
return { promise, cleanup };
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Pipe an output stream through an encoder to the channel.
|
|
14
43
|
*
|
|
15
44
|
* Returns when the stream completes, is cancelled (via signal), or errors.
|
|
16
45
|
* The `reason` field of the result indicates which case occurred.
|
|
17
|
-
* @param stream - The
|
|
18
|
-
* @param encoder - The
|
|
19
|
-
* @param signal -
|
|
20
|
-
* @param
|
|
46
|
+
* @param stream - The output stream to read from.
|
|
47
|
+
* @param encoder - The encoder to publish outputs through.
|
|
48
|
+
* @param signal - AbortSignal to monitor for cancellation.
|
|
49
|
+
* @param onCancelled - Optional callback invoked when the stream is cancelled, before the stream ends.
|
|
50
|
+
* @param resolveWriteOptions - Optional per-output hook returning {@link WriteOptions} overrides to pass to `encoder.publishOutput`.
|
|
21
51
|
* @param logger - Optional logger for diagnostic output.
|
|
22
|
-
* @returns
|
|
52
|
+
* @returns A {@link StreamResult}: `reason` is why the pipe ended, and `error` holds the caught error when `reason` is `'error'`.
|
|
23
53
|
*/
|
|
24
|
-
export const pipeStream = async <
|
|
25
|
-
stream: ReadableStream<
|
|
26
|
-
encoder:
|
|
54
|
+
export const pipeStream = async <TInput extends CodecInputEvent, TOutput extends CodecOutputEvent>(
|
|
55
|
+
stream: ReadableStream<TOutput>,
|
|
56
|
+
encoder: Encoder<TInput, TOutput>,
|
|
27
57
|
signal: AbortSignal | undefined,
|
|
28
|
-
|
|
58
|
+
onCancelled?: (write: (output: TOutput) => Promise<void>) => void | Promise<void>,
|
|
59
|
+
resolveWriteOptions?: (output: TOutput) => WriteOptions | undefined,
|
|
29
60
|
logger?: Logger,
|
|
30
61
|
): Promise<StreamResult> => {
|
|
31
62
|
logger?.trace('pipeStream();');
|
|
32
63
|
|
|
33
64
|
const reader = stream.getReader();
|
|
34
|
-
|
|
35
|
-
let abortListener: (() => void) | undefined;
|
|
36
|
-
const abortPromise = signal
|
|
37
|
-
? new Promise<void>((resolve) => {
|
|
38
|
-
if (signal.aborted) {
|
|
39
|
-
resolve();
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
abortListener = () => {
|
|
43
|
-
resolve();
|
|
44
|
-
};
|
|
45
|
-
signal.addEventListener('abort', abortListener, { once: true });
|
|
46
|
-
})
|
|
47
|
-
: // eslint-disable-next-line @typescript-eslint/no-empty-function -- never-resolving promise: no signal means no cancellation path
|
|
48
|
-
new Promise<void>(() => {});
|
|
65
|
+
const abort = abortSignalToPromise(signal);
|
|
49
66
|
|
|
50
67
|
let reason: StreamResult['reason'] = 'complete';
|
|
68
|
+
let caughtError: Error | undefined;
|
|
51
69
|
|
|
52
70
|
try {
|
|
53
71
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- intentional infinite loop broken by return/break
|
|
54
72
|
while (true) {
|
|
55
|
-
// .then() is intentional: transforms the
|
|
73
|
+
// .then() is intentional: transforms the AbortSignal into a discriminant
|
|
56
74
|
// for Promise.race — no async/await equivalent for this pattern.
|
|
57
|
-
const result = await Promise.race([reader.read(),
|
|
75
|
+
const result = await Promise.race([reader.read(), abort.promise.then(() => 'cancelled' as const)]);
|
|
58
76
|
|
|
59
|
-
if (result === '
|
|
77
|
+
if (result === 'cancelled') {
|
|
60
78
|
reason = 'cancelled';
|
|
61
|
-
logger?.debug('pipeStream(); stream cancelled by
|
|
62
|
-
if (
|
|
63
|
-
await
|
|
79
|
+
logger?.debug('pipeStream(); stream cancelled by AbortSignal');
|
|
80
|
+
if (onCancelled) {
|
|
81
|
+
await onCancelled(async (output: TOutput) => encoder.publishOutput(output));
|
|
64
82
|
}
|
|
65
|
-
await encoder.
|
|
83
|
+
await encoder.cancel('cancelled');
|
|
66
84
|
break;
|
|
67
85
|
}
|
|
68
86
|
|
|
@@ -73,12 +91,12 @@ export const pipeStream = async <TEvent, TMessage>(
|
|
|
73
91
|
break;
|
|
74
92
|
}
|
|
75
93
|
|
|
76
|
-
await encoder.
|
|
94
|
+
await encoder.publishOutput(value, resolveWriteOptions?.(value));
|
|
77
95
|
}
|
|
78
96
|
} catch (error) {
|
|
79
97
|
reason = 'error';
|
|
80
|
-
|
|
81
|
-
logger?.error('pipeStream(); stream error', { error:
|
|
98
|
+
caughtError = error instanceof Error ? error : new Error(String(error));
|
|
99
|
+
logger?.error('pipeStream(); stream error', { error: caughtError.message });
|
|
82
100
|
try {
|
|
83
101
|
await encoder.close();
|
|
84
102
|
} catch {
|
|
@@ -87,9 +105,9 @@ export const pipeStream = async <TEvent, TMessage>(
|
|
|
87
105
|
// the StreamResult reason ("error").
|
|
88
106
|
}
|
|
89
107
|
} finally {
|
|
90
|
-
|
|
108
|
+
abort.cleanup();
|
|
91
109
|
reader.releaseLock();
|
|
92
110
|
}
|
|
93
111
|
|
|
94
|
-
return { reason };
|
|
112
|
+
return { reason, error: caughtError };
|
|
95
113
|
};
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side run state management and lifecycle event publishing.
|
|
3
|
+
*
|
|
4
|
+
* Owns the authoritative run lifecycle. Tracks active runs with their
|
|
5
|
+
* AbortControllers and clientIds. Publishes run-start, run-resume, run-suspend, and
|
|
6
|
+
* run-end events on the Ably channel so all clients can react to run
|
|
7
|
+
* state changes.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type * as Ably from 'ably';
|
|
11
|
+
|
|
12
|
+
import { EVENT_RUN_END, EVENT_RUN_RESUME, EVENT_RUN_START, EVENT_RUN_SUSPEND } from '../../constants.js';
|
|
13
|
+
import type { Logger } from '../../logger.js';
|
|
14
|
+
import { buildLifecycleHeaders } from './headers.js';
|
|
15
|
+
import type { RunEndReason } from './types.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Per-invocation metadata carried on a run's opening lifecycle event. A
|
|
19
|
+
* continuation (re-entering an existing run) sets `continuation` and omits the
|
|
20
|
+
* structural `parent` / `forkOf` / `regenerates` fields.
|
|
21
|
+
*/
|
|
22
|
+
export interface StartRunMetadata {
|
|
23
|
+
/** Structural parent codec-message-id (fresh run-start only). */
|
|
24
|
+
parent?: string;
|
|
25
|
+
/** Forked user-prompt codec-message-id for an edit (fresh run-start only). */
|
|
26
|
+
forkOf?: string;
|
|
27
|
+
/** Regenerated assistant codec-message-id (fresh run-start only). */
|
|
28
|
+
regenerates?: string;
|
|
29
|
+
/** Agent-minted invocation id, carried on the lifecycle event. */
|
|
30
|
+
invocationId?: string;
|
|
31
|
+
/** ClientId of the triggering input event. */
|
|
32
|
+
inputClientId?: string;
|
|
33
|
+
/** Codec-message-id of the triggering input event. */
|
|
34
|
+
inputCodecMessageId?: string;
|
|
35
|
+
/** When true, publish `ai-run-resume` (re-entry) instead of `ai-run-start`. */
|
|
36
|
+
continuation?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Interface
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
/** Manages active runs and publishes run lifecycle events on the channel. */
|
|
44
|
+
export interface RunManager {
|
|
45
|
+
/**
|
|
46
|
+
* Register a run and publish its opening lifecycle event. Publishes
|
|
47
|
+
* `ai-run-start` for a fresh run, or `ai-run-resume` when `metadata.continuation`
|
|
48
|
+
* is set (a subsequent invocation re-entering an existing run). A resume omits
|
|
49
|
+
* the structural `parent` / `forkOf` / `regenerates` headers — the original
|
|
50
|
+
* run-start owns the run's structure. Returns the run's AbortSignal.
|
|
51
|
+
*/
|
|
52
|
+
startRun(
|
|
53
|
+
runId: string,
|
|
54
|
+
clientId?: string,
|
|
55
|
+
controller?: AbortController,
|
|
56
|
+
metadata?: StartRunMetadata,
|
|
57
|
+
): Promise<AbortSignal>;
|
|
58
|
+
/**
|
|
59
|
+
* Suspend a run. Publishes run-suspend on the channel and drops the run's
|
|
60
|
+
* active-run entry — the agent process terminates on suspend, so there is no
|
|
61
|
+
* live AbortController to retain. A cancel arriving during suspension is a
|
|
62
|
+
* no-op; the resuming invocation re-registers the run via {@link startRun}.
|
|
63
|
+
* Carries the same per-invocation attribution as {@link endRun}
|
|
64
|
+
* (`inputClientId`, `inputCodecMessageId`), since a suspend is the terminal
|
|
65
|
+
* event of the suspending invocation just as run-end is of an ending one.
|
|
66
|
+
*/
|
|
67
|
+
suspendRun(runId: string, invocationId?: string, inputClientId?: string, inputCodecMessageId?: string): Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* End a run. Publishes run-end on the channel (stamping `reason` as the
|
|
70
|
+
* run-reason header) and drops the run's active-run entry. Carries the same
|
|
71
|
+
* per-invocation attribution as {@link suspendRun} (`invocationId`,
|
|
72
|
+
* `inputClientId`, `inputCodecMessageId`), since run-end is the terminal event
|
|
73
|
+
* of the ending invocation.
|
|
74
|
+
*/
|
|
75
|
+
endRun(
|
|
76
|
+
runId: string,
|
|
77
|
+
reason: RunEndReason,
|
|
78
|
+
invocationId?: string,
|
|
79
|
+
inputClientId?: string,
|
|
80
|
+
inputCodecMessageId?: string,
|
|
81
|
+
): Promise<void>;
|
|
82
|
+
/** Get the AbortSignal for a run. */
|
|
83
|
+
getSignal(runId: string): AbortSignal | undefined;
|
|
84
|
+
/** Get the clientId that owns a run. */
|
|
85
|
+
getClientId(runId: string): string | undefined;
|
|
86
|
+
/** Fire the AbortSignal for a run to cancel any in-flight work. */
|
|
87
|
+
cancel(runId: string): void;
|
|
88
|
+
/** Get all active run IDs. */
|
|
89
|
+
getActiveRunIds(): string[];
|
|
90
|
+
/** Cancel all active runs and clear state. */
|
|
91
|
+
close(): void;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Internal state
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
interface ActiveRunEntry {
|
|
99
|
+
controller: AbortController;
|
|
100
|
+
clientId: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Implementation
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
class DefaultRunManager implements RunManager {
|
|
108
|
+
private readonly _channel: Ably.RealtimeChannel;
|
|
109
|
+
private readonly _logger: Logger | undefined;
|
|
110
|
+
private readonly _activeRuns = new Map<string, ActiveRunEntry>();
|
|
111
|
+
|
|
112
|
+
constructor(channel: Ably.RealtimeChannel, logger?: Logger) {
|
|
113
|
+
this._channel = channel;
|
|
114
|
+
this._logger = logger?.withContext({ component: 'RunManager' });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async startRun(
|
|
118
|
+
runId: string,
|
|
119
|
+
clientId?: string,
|
|
120
|
+
externalController?: AbortController,
|
|
121
|
+
metadata?: StartRunMetadata,
|
|
122
|
+
): Promise<AbortSignal> {
|
|
123
|
+
this._logger?.trace('DefaultRunManager.startRun();', { runId, clientId });
|
|
124
|
+
|
|
125
|
+
const controller = externalController ?? new AbortController();
|
|
126
|
+
const resolvedClientId = clientId ?? '';
|
|
127
|
+
this._activeRuns.set(runId, { controller, clientId: resolvedClientId });
|
|
128
|
+
|
|
129
|
+
// A continuation re-enters an already-started run: publish `ai-run-resume`
|
|
130
|
+
// rather than `ai-run-start`. Resume is a pure re-entry signal — the
|
|
131
|
+
// original run-start already established the run's structure, so the
|
|
132
|
+
// parent / forkOf / regenerates metadata is NOT re-stamped here (doing so
|
|
133
|
+
// would point the run at content within itself). The agent learned this is
|
|
134
|
+
// a continuation from the run-id on the triggering input; the re-entry is
|
|
135
|
+
// conveyed to clients by the event name, not a header echo. The
|
|
136
|
+
// invocation-id / input attribution headers are carried on both.
|
|
137
|
+
const continuation = metadata?.continuation === true;
|
|
138
|
+
|
|
139
|
+
const headers = buildLifecycleHeaders({
|
|
140
|
+
runId,
|
|
141
|
+
runClientId: resolvedClientId,
|
|
142
|
+
parent: continuation ? undefined : metadata?.parent,
|
|
143
|
+
forkOf: continuation ? undefined : metadata?.forkOf,
|
|
144
|
+
regenerates: continuation ? undefined : metadata?.regenerates,
|
|
145
|
+
invocationId: metadata?.invocationId,
|
|
146
|
+
inputClientId: metadata?.inputClientId,
|
|
147
|
+
inputCodecMessageId: metadata?.inputCodecMessageId,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
await this._channel.publish({
|
|
151
|
+
name: continuation ? EVENT_RUN_RESUME : EVENT_RUN_START,
|
|
152
|
+
extras: { ai: { transport: headers } },
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
this._logger?.debug('DefaultRunManager.startRun(); run started', { runId });
|
|
156
|
+
return controller.signal;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async suspendRun(
|
|
160
|
+
runId: string,
|
|
161
|
+
invocationId?: string,
|
|
162
|
+
inputClientId?: string,
|
|
163
|
+
inputCodecMessageId?: string,
|
|
164
|
+
): Promise<void> {
|
|
165
|
+
this._logger?.trace('DefaultRunManager.suspendRun();', { runId });
|
|
166
|
+
await this._publishTerminal(EVENT_RUN_SUSPEND, runId, { invocationId, inputClientId, inputCodecMessageId });
|
|
167
|
+
this._logger?.debug('DefaultRunManager.suspendRun(); run suspended', { runId });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async endRun(
|
|
171
|
+
runId: string,
|
|
172
|
+
reason: RunEndReason,
|
|
173
|
+
invocationId?: string,
|
|
174
|
+
inputClientId?: string,
|
|
175
|
+
inputCodecMessageId?: string,
|
|
176
|
+
): Promise<void> {
|
|
177
|
+
this._logger?.trace('DefaultRunManager.endRun();', { runId, reason });
|
|
178
|
+
await this._publishTerminal(EVENT_RUN_END, runId, { reason, invocationId, inputClientId, inputCodecMessageId });
|
|
179
|
+
this._logger?.debug('DefaultRunManager.endRun(); run ended', { runId, reason });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Publish a run's terminal lifecycle event (run-suspend or run-end) and drop
|
|
184
|
+
* its active-run entry. Both events are the suspending/ending invocation's
|
|
185
|
+
* terminal signal, carrying the same per-invocation correlation; they differ
|
|
186
|
+
* only by event name and the run-reason header (run-end). Publishes BEFORE
|
|
187
|
+
* dropping local state so a publish failure leaves the run in the active set.
|
|
188
|
+
* @param eventName - The lifecycle event to publish (run-suspend or run-end).
|
|
189
|
+
* @param runId - The run being suspended or ended.
|
|
190
|
+
* @param attribution - Per-invocation correlation and the terminal reason.
|
|
191
|
+
* @param attribution.reason - Terminal reason; set for run-end, omitted for run-suspend.
|
|
192
|
+
* @param attribution.invocationId - The invocation's id.
|
|
193
|
+
* @param attribution.inputClientId - ClientId of the triggering input event.
|
|
194
|
+
* @param attribution.inputCodecMessageId - Codec-message-id of the triggering input event.
|
|
195
|
+
*/
|
|
196
|
+
private async _publishTerminal(
|
|
197
|
+
eventName: string,
|
|
198
|
+
runId: string,
|
|
199
|
+
attribution: {
|
|
200
|
+
reason?: RunEndReason;
|
|
201
|
+
invocationId?: string;
|
|
202
|
+
inputClientId?: string;
|
|
203
|
+
inputCodecMessageId?: string;
|
|
204
|
+
},
|
|
205
|
+
): Promise<void> {
|
|
206
|
+
const resolvedClientId = this._activeRuns.get(runId)?.clientId ?? '';
|
|
207
|
+
const headers = buildLifecycleHeaders({ runId, runClientId: resolvedClientId, ...attribution });
|
|
208
|
+
await this._channel.publish({ name: eventName, extras: { ai: { transport: headers } } });
|
|
209
|
+
this._activeRuns.delete(runId);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
getSignal(runId: string): AbortSignal | undefined {
|
|
213
|
+
return this._activeRuns.get(runId)?.controller.signal;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
getClientId(runId: string): string | undefined {
|
|
217
|
+
return this._activeRuns.get(runId)?.clientId;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
cancel(runId: string): void {
|
|
221
|
+
this._logger?.debug('DefaultRunManager.cancel();', { runId });
|
|
222
|
+
this._activeRuns.get(runId)?.controller.abort();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
getActiveRunIds(): string[] {
|
|
226
|
+
return [...this._activeRuns.keys()];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
close(): void {
|
|
230
|
+
this._logger?.trace('DefaultRunManager.close();', { activeRuns: this._activeRuns.size });
|
|
231
|
+
for (const state of this._activeRuns.values()) {
|
|
232
|
+
state.controller.abort();
|
|
233
|
+
}
|
|
234
|
+
this._activeRuns.clear();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
// Factory
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Create a run manager bound to the given channel.
|
|
244
|
+
* @param channel - The Ably channel to publish lifecycle events on.
|
|
245
|
+
* @param logger - Optional logger for diagnostic output.
|
|
246
|
+
* @returns A new {@link RunManager} instance.
|
|
247
|
+
*/
|
|
248
|
+
export const createRunManager = (channel: Ably.RealtimeChannel, logger?: Logger): RunManager =>
|
|
249
|
+
new DefaultRunManager(channel, logger);
|