@ably/ai-transport 0.2.0 → 0.3.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 +10 -19
- package/dist/ably-ai-transport.js +1790 -1091
- 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 +2 -2
- package/dist/core/agent.d.ts +20 -5
- package/dist/core/channel-options.d.ts +57 -0
- package/dist/core/codec/codec-event.d.ts +9 -0
- package/dist/core/codec/decoder.d.ts +4 -1
- package/dist/core/codec/define-codec.d.ts +100 -0
- package/dist/core/codec/encoder.d.ts +2 -7
- package/dist/core/codec/field-bag.d.ts +85 -0
- package/dist/core/codec/fields.d.ts +141 -0
- package/dist/core/codec/index.d.ts +8 -1
- package/dist/core/codec/input-descriptor-decoder.d.ts +19 -0
- package/dist/core/codec/input-descriptor-encoder.d.ts +22 -0
- package/dist/core/codec/input-descriptors.d.ts +281 -0
- package/dist/core/codec/output-descriptor-decoder.d.ts +29 -0
- package/dist/core/codec/output-descriptor-encoder.d.ts +31 -0
- package/dist/core/codec/output-descriptors.d.ts +237 -0
- package/dist/core/codec/types.d.ts +95 -36
- package/dist/core/codec/well-known-inputs.d.ts +52 -0
- package/dist/core/transport/agent-view.d.ts +296 -0
- package/dist/core/transport/decode-fold.d.ts +40 -32
- package/dist/core/transport/headers.d.ts +30 -1
- package/dist/core/transport/index.d.ts +1 -1
- package/dist/core/transport/invocation.d.ts +1 -1
- package/dist/core/transport/load-history-pages.d.ts +71 -0
- package/dist/core/transport/load-history.d.ts +21 -16
- package/dist/core/transport/run-manager.d.ts +9 -11
- package/dist/core/transport/session-support.d.ts +55 -0
- package/dist/core/transport/tree.d.ts +165 -15
- package/dist/core/transport/types/agent.d.ts +120 -98
- package/dist/core/transport/types/client.d.ts +45 -12
- package/dist/core/transport/types/tree.d.ts +52 -10
- package/dist/core/transport/types/view.d.ts +55 -28
- package/dist/core/transport/view.d.ts +176 -58
- package/dist/core/transport/wire-log.d.ts +102 -0
- package/dist/errors.d.ts +10 -4
- package/dist/index.d.ts +6 -5
- package/dist/react/ably-ai-transport-react.js +784 -415
- 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 +2 -1
- package/dist/react/contexts/client-session-provider.d.ts +3 -0
- package/dist/react/index.d.ts +2 -1
- package/dist/react/internal/skipped-session.d.ts +8 -0
- package/dist/react/use-view.d.ts +3 -3
- package/dist/utils.d.ts +22 -54
- package/dist/vercel/ably-ai-transport-vercel.js +2297 -2026
- 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/decode-lifecycle.d.ts +9 -0
- package/dist/vercel/codec/events.d.ts +1 -2
- package/dist/vercel/codec/fields.d.ts +44 -0
- package/dist/vercel/codec/fold-content.d.ts +16 -0
- package/dist/vercel/codec/fold-data.d.ts +16 -0
- package/dist/vercel/codec/fold-input.d.ts +67 -0
- package/dist/vercel/codec/fold-lifecycle.d.ts +16 -0
- package/dist/vercel/codec/fold-text.d.ts +16 -0
- package/dist/vercel/codec/fold-tool-input.d.ts +17 -0
- package/dist/vercel/codec/fold-tool-output.d.ts +16 -0
- package/dist/vercel/codec/index.d.ts +5 -30
- package/dist/vercel/codec/inputs.d.ts +11 -0
- package/dist/vercel/codec/outputs.d.ts +11 -0
- package/dist/vercel/codec/reducer-state.d.ts +121 -0
- package/dist/vercel/codec/reducer.d.ts +20 -102
- package/dist/vercel/codec/tool-transitions.d.ts +0 -6
- package/dist/vercel/codec/wire-data.d.ts +34 -0
- package/dist/vercel/index.d.ts +1 -0
- package/dist/vercel/react/ably-ai-transport-vercel-react.js +2013 -9500
- package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +1 -70
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
- package/dist/vercel/react/contexts/chat-transport-context.d.ts +2 -1
- package/dist/vercel/run-end-reason.d.ts +66 -11
- package/dist/vercel/tool-part.d.ts +21 -0
- package/dist/vercel/transport/chat-transport.d.ts +0 -2
- package/dist/vercel/transport/index.d.ts +1 -1
- package/dist/vercel/transport/run-output-stream.d.ts +6 -8
- package/dist/version.d.ts +1 -1
- package/package.json +2 -2
- package/src/constants.ts +2 -2
- package/src/core/agent.ts +43 -19
- package/src/core/channel-options.ts +89 -0
- package/src/core/codec/codec-event.ts +27 -0
- package/src/core/codec/decoder.ts +145 -21
- package/src/core/codec/define-codec.ts +432 -0
- package/src/core/codec/encoder.ts +13 -54
- package/src/core/codec/field-bag.ts +142 -0
- package/src/core/codec/fields.ts +193 -0
- package/src/core/codec/index.ts +43 -0
- package/src/core/codec/input-descriptor-decoder.ts +97 -0
- package/src/core/codec/input-descriptor-encoder.ts +150 -0
- package/src/core/codec/input-descriptors.ts +373 -0
- package/src/core/codec/output-descriptor-decoder.ts +139 -0
- package/src/core/codec/output-descriptor-encoder.ts +101 -0
- package/src/core/codec/output-descriptors.ts +307 -0
- package/src/core/codec/types.ts +99 -36
- package/src/core/codec/well-known-inputs.ts +96 -0
- package/src/core/transport/agent-session.ts +330 -589
- package/src/core/transport/agent-view.ts +738 -0
- package/src/core/transport/client-session.ts +74 -69
- package/src/core/transport/decode-fold.ts +57 -47
- package/src/core/transport/headers.ts +57 -4
- package/src/core/transport/index.ts +2 -1
- package/src/core/transport/invocation.ts +1 -1
- package/src/core/transport/load-history-pages.ts +220 -0
- package/src/core/transport/load-history.ts +63 -61
- package/src/core/transport/pipe-stream.ts +10 -1
- package/src/core/transport/run-manager.ts +25 -31
- package/src/core/transport/session-support.ts +96 -0
- package/src/core/transport/tree.ts +414 -47
- package/src/core/transport/types/agent.ts +129 -102
- package/src/core/transport/types/client.ts +49 -13
- package/src/core/transport/types/tree.ts +61 -12
- package/src/core/transport/types/view.ts +57 -28
- package/src/core/transport/view.ts +520 -172
- package/src/core/transport/wire-log.ts +189 -0
- package/src/errors.ts +10 -3
- package/src/index.ts +44 -11
- package/src/react/contexts/client-session-context.ts +1 -1
- package/src/react/contexts/client-session-provider.tsx +38 -2
- package/src/react/index.ts +2 -1
- package/src/react/internal/skipped-session.ts +62 -0
- package/src/react/use-client-session.ts +7 -30
- package/src/react/use-view.ts +3 -3
- package/src/utils.ts +31 -97
- package/src/vercel/codec/decode-lifecycle.ts +70 -0
- package/src/vercel/codec/events.ts +1 -3
- package/src/vercel/codec/fields.ts +58 -0
- package/src/vercel/codec/fold-content.ts +54 -0
- package/src/vercel/codec/fold-data.ts +46 -0
- package/src/vercel/codec/fold-input.ts +255 -0
- package/src/vercel/codec/fold-lifecycle.ts +85 -0
- package/src/vercel/codec/fold-text.ts +55 -0
- package/src/vercel/codec/fold-tool-input.ts +86 -0
- package/src/vercel/codec/fold-tool-output.ts +79 -0
- package/src/vercel/codec/index.ts +23 -63
- package/src/vercel/codec/inputs.ts +116 -0
- package/src/vercel/codec/outputs.ts +207 -0
- package/src/vercel/codec/reducer-state.ts +169 -0
- package/src/vercel/codec/reducer.ts +52 -838
- package/src/vercel/codec/tool-transitions.ts +1 -12
- package/src/vercel/codec/wire-data.ts +64 -0
- package/src/vercel/index.ts +1 -0
- package/src/vercel/react/contexts/chat-transport-context.ts +1 -1
- package/src/vercel/react/use-chat-transport.ts +8 -28
- package/src/vercel/react/use-message-sync.ts +5 -10
- package/src/vercel/run-end-reason.ts +95 -16
- package/src/vercel/tool-part.ts +25 -0
- package/src/vercel/transport/chat-transport.ts +10 -22
- package/src/vercel/transport/index.ts +1 -1
- package/src/vercel/transport/run-output-stream.ts +7 -8
- package/src/version.ts +1 -1
- package/dist/core/transport/branch-chain.d.ts +0 -43
- package/dist/core/transport/load-conversation.d.ts +0 -128
- package/dist/vercel/codec/decoder.d.ts +0 -9
- package/dist/vercel/codec/encoder.d.ts +0 -11
- package/src/core/transport/branch-chain.ts +0 -58
- package/src/core/transport/load-conversation.ts +0 -355
- package/src/vercel/codec/decoder.ts +0 -696
- package/src/vercel/codec/encoder.ts +0 -548
|
@@ -19,7 +19,7 @@ export interface ChatTransportSlot {
|
|
|
19
19
|
* The shape of the single {@link ChatTransportContext} value.
|
|
20
20
|
* Combines the nearest slot with the full registry in one context object.
|
|
21
21
|
*/
|
|
22
|
-
|
|
22
|
+
interface ChatTransportContextValue {
|
|
23
23
|
/** The slot from the nearest {@link ChatTransportProvider} in the tree. */
|
|
24
24
|
readonly nearest: ChatTransportSlot | undefined;
|
|
25
25
|
/** All registered slots, keyed by channelName. */
|
|
@@ -31,3 +31,4 @@ export interface ChatTransportContextValue {
|
|
|
31
31
|
* read by {@link useChatTransport}.
|
|
32
32
|
*/
|
|
33
33
|
export declare const ChatTransportContext: import('react').Context<ChatTransportContextValue>;
|
|
34
|
+
export {};
|
|
@@ -1,14 +1,69 @@
|
|
|
1
1
|
import { RunEndReason, StreamResult } from '../core/transport/types.js';
|
|
2
|
+
import * as Ably from 'ably';
|
|
2
3
|
import type * as AI from 'ai';
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
-
* `
|
|
6
|
-
* `
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* Vercel
|
|
10
|
-
*
|
|
11
|
-
*
|
|
5
|
+
* The outcome of a Vercel `streamText` response piped through `Run.pipe`.
|
|
6
|
+
* Discriminated on `reason`: `'suspend'` means the run should pause; the
|
|
7
|
+
* non-`'suspend'` arms describe how it terminated, and an `'error'` outcome
|
|
8
|
+
* always carries `error`.
|
|
9
|
+
*
|
|
10
|
+
* This is a *description of what the Vercel run resulted in*, not a command to
|
|
11
|
+
* the SDK. The common case maps cleanly onto one transport action — `'suspend'`
|
|
12
|
+
* → `Run.suspend()`, everything else → `Run.end()` — and to make that case a
|
|
13
|
+
* one-liner the non-`'suspend'` arms are deliberately assignable to
|
|
14
|
+
* {@link RunEndParams}, so after a `suspend` guard the whole object passes
|
|
15
|
+
* straight to `Run.end(outcome)`. That assignability is a convenience for this
|
|
16
|
+
* adapter, not a constraint on what an outcome can mean: responding to an
|
|
17
|
+
* outcome may also involve work outside this SDK (persisting a result,
|
|
18
|
+
* notifying a human, triggering a downstream workflow), and the developer is
|
|
19
|
+
* free to do that around the terminal call.
|
|
20
|
+
*
|
|
21
|
+
* The type is Vercel-specific by design. Outcomes are the layer where agent
|
|
22
|
+
* SDKs diverge most — both in what they report (the `'suspend'` arm exists only
|
|
23
|
+
* because Vercel surfaces unexecuted tool calls as a non-terminal finish) and
|
|
24
|
+
* in what a developer must do in response. A different SDK's outcome type would
|
|
25
|
+
* have different arms; hence each adapter names its own rather than sharing a
|
|
26
|
+
* single core `RunOutcome`. The vocabulary it bottoms out in
|
|
27
|
+
* ({@link RunEndParams}, `Run.suspend`/`Run.end`) is the shared, codec-agnostic
|
|
28
|
+
* part that does live in core.
|
|
29
|
+
*/
|
|
30
|
+
export type VercelRunOutcome = {
|
|
31
|
+
/**
|
|
32
|
+
* The LLM requested tools the SDK did not auto-execute, so the run
|
|
33
|
+
* pauses rather than ending — call `Run.suspend()`.
|
|
34
|
+
*/
|
|
35
|
+
reason: 'suspend';
|
|
36
|
+
/** Never present for a suspend outcome. */
|
|
37
|
+
error?: never;
|
|
38
|
+
} | {
|
|
39
|
+
/** A non-error terminal reason; pass the outcome to `Run.end()`. */
|
|
40
|
+
reason: Exclude<RunEndReason, 'error'>;
|
|
41
|
+
/** Never present for a non-error outcome. */
|
|
42
|
+
error?: never;
|
|
43
|
+
} | {
|
|
44
|
+
/** The run ended in error; pass the outcome to `Run.end()`. */
|
|
45
|
+
reason: Extract<RunEndReason, 'error'>;
|
|
46
|
+
/**
|
|
47
|
+
* The terminal error: the underlying stream / `finishReason` failure
|
|
48
|
+
* wrapped as an `Ably.ErrorInfo` (code `StreamError`).
|
|
49
|
+
*/
|
|
50
|
+
error: Ably.ErrorInfo;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Derive the {@link VercelRunOutcome} for a Vercel `streamText` response that
|
|
54
|
+
* was piped through `Run.pipe`. Preserves transport-level outcomes
|
|
55
|
+
* (`'cancelled'`, `'error'`) from the pipe result; when the pipe completed
|
|
56
|
+
* naturally, awaits Vercel's `finishReason` and returns `'suspend'` for
|
|
57
|
+
* `'tool-calls'` (the LLM requested tools the SDK did not auto-execute, so the
|
|
58
|
+
* run should suspend rather than end), or `'complete'` otherwise.
|
|
59
|
+
*
|
|
60
|
+
* Surfaces the failure for both error shapes so the caller can forward it to
|
|
61
|
+
* `Run.end(reason, error)`: a stream that threw (`pipeResult.error`) and a
|
|
62
|
+
* `finishReason` that rejected with a non-abort error (e.g.
|
|
63
|
+
* `NoOutputGeneratedError`, network blow-ups). The error is wrapped as an
|
|
64
|
+
* `Ably.ErrorInfo` (code `StreamError`). A stream that already produced a
|
|
65
|
+
* codec-level error chunk is unaffected — stamping run-end is the
|
|
66
|
+
* codec-agnostic baseline that any consumer can read.
|
|
12
67
|
*
|
|
13
68
|
* Tolerates `finishReason` rejection. Vercel AI SDK v6 rejects
|
|
14
69
|
* `streamText().finishReason` with the abort signal's reason when the stream
|
|
@@ -23,7 +78,7 @@ import type * as AI from 'ai';
|
|
|
23
78
|
* of every route handler.
|
|
24
79
|
* @param pipeResult - The result returned by `Run.pipe`.
|
|
25
80
|
* @param finishReason - The `finishReason` promise from a `streamText` result.
|
|
26
|
-
* @returns
|
|
27
|
-
*
|
|
81
|
+
* @returns The {@link VercelRunOutcome}: the terminal `reason` (or `'suspend'`)
|
|
82
|
+
* and, when `reason` is `'error'`, the wrapped `error` to pass to `Run.end`.
|
|
28
83
|
*/
|
|
29
|
-
export declare const vercelRunOutcome: (pipeResult: StreamResult, finishReason: PromiseLike<AI.FinishReason>) => Promise<
|
|
84
|
+
export declare const vercelRunOutcome: (pipeResult: StreamResult, finishReason: PromiseLike<AI.FinishReason>) => Promise<VercelRunOutcome>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared tool-part type guard for the Vercel layer.
|
|
3
|
+
*
|
|
4
|
+
* The codec normalises every tool part to the `dynamic-tool` shape, but the AI
|
|
5
|
+
* SDK emits `tool-${name}` parts for statically-declared tools. Both shapes
|
|
6
|
+
* carry `toolCallId` and `state`. The guard accepts either representation so
|
|
7
|
+
* the transport's unresolved-tool detection and the React overlay merge can
|
|
8
|
+
* match tool parts uniformly — and so the cross-representation rule lives in
|
|
9
|
+
* one place rather than being re-spelled per call site.
|
|
10
|
+
*/
|
|
11
|
+
import type * as AI from 'ai';
|
|
12
|
+
/** A UIMessage tool part in either the `dynamic-tool` or `tool-${name}` representation. */
|
|
13
|
+
export type ToolPart = AI.DynamicToolUIPart | AI.ToolUIPart;
|
|
14
|
+
/**
|
|
15
|
+
* Whether a UIMessage part is a tool part of either representation. The
|
|
16
|
+
* `toolCallId`/`state` shape check is defensive against a future AI SDK release
|
|
17
|
+
* introducing a non-tool variant under the `tool-` prefix (none exists today).
|
|
18
|
+
* @param part - The UIMessage part to inspect.
|
|
19
|
+
* @returns True when the part is a tool part.
|
|
20
|
+
*/
|
|
21
|
+
export declare const isToolPart: (part: AI.UIMessage["parts"][number]) => part is ToolPart;
|
|
@@ -55,8 +55,6 @@ export interface SendMessagesRequestContext {
|
|
|
55
55
|
/** The codec-message-id of the predecessor in the conversation thread. */
|
|
56
56
|
parent?: string;
|
|
57
57
|
}
|
|
58
|
-
/** Default agent endpoint the transport POSTs invocations to — mirrors Vercel's DefaultChatTransport. */
|
|
59
|
-
export declare const DEFAULT_VERCEL_API = "/api/chat";
|
|
60
58
|
/** Options for customizing the ChatTransport behavior. */
|
|
61
59
|
export interface ChatTransportOptions {
|
|
62
60
|
/**
|
|
@@ -14,7 +14,7 @@ import { VercelInput, VercelOutput, VercelProjection } from '../codec/index.js';
|
|
|
14
14
|
* ```
|
|
15
15
|
*/
|
|
16
16
|
export type { ChatTransport, ChatTransportOptions, SendMessagesRequestContext } from './chat-transport.js';
|
|
17
|
-
export { createChatTransport
|
|
17
|
+
export { createChatTransport } from './chat-transport.js';
|
|
18
18
|
import type * as AI from 'ai';
|
|
19
19
|
/** Core client session options with Vercel AI SDK types pre-applied. */
|
|
20
20
|
type CoreClientOpts = ClientSessionOptions<VercelInput, VercelOutput, VercelProjection, AI.UIMessage>;
|
|
@@ -23,23 +23,21 @@ import { VercelInput, VercelOutput, VercelProjection } from '../codec/index.js';
|
|
|
23
23
|
* continuity loss, or an agent-reported mid-run error), so the consumer's
|
|
24
24
|
* reader rejects rather than hanging.
|
|
25
25
|
*/
|
|
26
|
-
import * as Ably from 'ably';
|
|
27
26
|
import type * as AI from 'ai';
|
|
28
27
|
type VercelSession = ClientSession<VercelInput, VercelOutput, VercelProjection, AI.UIMessage>;
|
|
29
|
-
/** A consumer-facing run output stream plus the
|
|
30
|
-
|
|
28
|
+
/** A consumer-facing run output stream plus the handle to close it externally. */
|
|
29
|
+
interface RunOutputStream {
|
|
31
30
|
/** The stream of decoded outputs for the run, as `useChat` consumes it. */
|
|
32
31
|
stream: ReadableStream<VercelOutput>;
|
|
33
32
|
/** Close the stream now (e.g. on local cancel). Idempotent. */
|
|
34
33
|
close: () => void;
|
|
35
|
-
/** Error the stream now (e.g. on a failed agent-invocation POST). Idempotent. */
|
|
36
|
-
error: (reason: Ably.ErrorInfo) => void;
|
|
37
34
|
}
|
|
38
35
|
/**
|
|
39
36
|
* Create a consumer-facing output stream for a send, sourced from the session
|
|
40
37
|
* Tree's events. See the module docs for close/error semantics. The returned
|
|
41
|
-
* `close
|
|
42
|
-
*
|
|
38
|
+
* `close` lets the caller settle the stream for conditions the Tree doesn't
|
|
39
|
+
* surface (local cancel). Session errors are wired internally to error the
|
|
40
|
+
* stream.
|
|
43
41
|
*
|
|
44
42
|
* Outputs route PURELY by the triggering input's codec-message-id — the key the
|
|
45
43
|
* client owns from send time, before the agent mints the runId. The agent's
|
|
@@ -50,7 +48,7 @@ export interface RunOutputStream {
|
|
|
50
48
|
* Used only by the run-end safety-net; routing keys on `inputCodecMessageId`.
|
|
51
49
|
* @param inputCodecMessageId - The triggering input's codec-message-id. An
|
|
52
50
|
* output routes to this stream when it carries this id.
|
|
53
|
-
* @returns The stream and its external
|
|
51
|
+
* @returns The stream and its external close handle.
|
|
54
52
|
*/
|
|
55
53
|
export declare const createRunOutputStream: (session: VercelSession, runId: Promise<string>, inputCodecMessageId: string) => RunOutputStream;
|
|
56
54
|
export {};
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** SDK version. Kept in sync with `package.json` by the `/release` workflow. */
|
|
2
|
-
export declare const VERSION = "0.
|
|
2
|
+
export declare const VERSION = "0.3.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ably/ai-transport",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Ably transport and codecs for building AI applications with Ably.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/ably-ai-transport.umd.cjs",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
}
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
73
|
-
"ably": "^2.
|
|
73
|
+
"ably": "^2.23.0",
|
|
74
74
|
"ai": "^6",
|
|
75
75
|
"react": "^18.0.0 || ^19.0.0"
|
|
76
76
|
},
|
package/src/constants.ts
CHANGED
|
@@ -164,14 +164,14 @@ export const EVENT_RUN_END = 'ai-run-end';
|
|
|
164
164
|
* Message name: every agent-published codec event (text, reasoning, tool calls,
|
|
165
165
|
* tool outputs, lifecycle helpers, file / source parts, data-* chunks) rides
|
|
166
166
|
* this single wire name. The codec event's own `type` is carried in the
|
|
167
|
-
* codec-level `
|
|
167
|
+
* SDK-controlled codec-level `kind` header so the decoder can dispatch.
|
|
168
168
|
*/
|
|
169
169
|
export const EVENT_AI_OUTPUT = 'ai-output';
|
|
170
170
|
|
|
171
171
|
/**
|
|
172
172
|
* Message name: every client-published codec event (user-message parts,
|
|
173
173
|
* tool-approval responses, regenerate signals) rides this single wire
|
|
174
|
-
* name. The codec event's own kind is carried in the codec-level `
|
|
174
|
+
* name. The codec event's own kind is carried in the codec-level `kind`
|
|
175
175
|
* header so the decoder can dispatch.
|
|
176
176
|
*/
|
|
177
177
|
export const EVENT_AI_INPUT = 'ai-input';
|
package/src/core/agent.ts
CHANGED
|
@@ -19,11 +19,6 @@ interface RealtimeWithOptions extends Ably.Realtime {
|
|
|
19
19
|
|
|
20
20
|
const SDK_NAME = 'ai-transport-js';
|
|
21
21
|
|
|
22
|
-
/** Internal shape a codec may carry to opt into Ably-Agent header registration. */
|
|
23
|
-
interface AdapterTagHolder {
|
|
24
|
-
readonly adapterTag?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
22
|
/**
|
|
28
23
|
* Merge `agents` into `client.options.agents` and return the space-separated
|
|
29
24
|
* `params.agent` string for channel ATTACH.
|
|
@@ -40,29 +35,58 @@ const injectAgents = (
|
|
|
40
35
|
): { params: { agent: string } } => {
|
|
41
36
|
const realtime = client as RealtimeWithOptions;
|
|
42
37
|
realtime.options.agents = { ...realtime.options.agents, ...agents };
|
|
43
|
-
|
|
38
|
+
return { params: { agent: joinAgents(agents) } };
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Build the agent-name → version map this SDK registers for the given codec.
|
|
43
|
+
* @param codec - The codec instance whose optional identifier opts into registration.
|
|
44
|
+
* @param codec.adapterTag - The optional Ably-Agent identifier; registered as an agent when present.
|
|
45
|
+
* @returns Map of agent name to version, always including this SDK.
|
|
46
|
+
*/
|
|
47
|
+
const buildAgents = (codec?: { readonly adapterTag?: string }): Record<string, string> => {
|
|
48
|
+
const adapterTag = codec?.adapterTag;
|
|
49
|
+
const agents: Record<string, string> = { [SDK_NAME]: VERSION };
|
|
50
|
+
if (adapterTag) agents[adapterTag] = VERSION;
|
|
51
|
+
return agents;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Render an agents map as the space-separated `name/version` string Ably expects.
|
|
56
|
+
* @param agents - Map of agent name to version.
|
|
57
|
+
* @returns The space-separated `name/version` agent string.
|
|
58
|
+
*/
|
|
59
|
+
const joinAgents = (agents: Record<string, string>): string =>
|
|
60
|
+
Object.entries(agents)
|
|
44
61
|
.map(([name, version]) => `${name}/${version}`)
|
|
45
62
|
.join(' ');
|
|
46
|
-
|
|
47
|
-
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The space-separated `params.agent` string this SDK stamps on channel ATTACH —
|
|
66
|
+
* `ai-transport-js/<version>` plus the codec's adapter tag when present. Pure:
|
|
67
|
+
* unlike {@link registerAgent} it does not mutate the client. Use it to seed a
|
|
68
|
+
* `<ChannelProvider options>` so ably-js's React hooks append their own agent
|
|
69
|
+
* additively (`channelOptionsForReactHooks`) rather than overwriting this SDK's.
|
|
70
|
+
* @param codec - The codec instance whose optional identifier opts into registration.
|
|
71
|
+
* @param codec.adapterTag - The optional Ably-Agent identifier; registered as an agent when present.
|
|
72
|
+
* @returns The channel `params.agent` string.
|
|
73
|
+
*/
|
|
74
|
+
export const channelAgent = (codec?: { readonly adapterTag?: string }): string => joinAgents(buildAgents(codec));
|
|
48
75
|
|
|
49
76
|
/**
|
|
50
77
|
* Register this SDK (and optionally a codec) on the supplied Realtime client
|
|
51
78
|
* and return the channel options the caller should pass to
|
|
52
79
|
* `client.channels.get(...)` so the agent is also carried on channel ATTACH.
|
|
53
|
-
* Sets `options.agents['ai-transport-js'] = VERSION`. When the codec carries
|
|
54
|
-
*
|
|
55
|
-
* `options.agents[adapterTag] = VERSION`.
|
|
80
|
+
* Sets `options.agents['ai-transport-js'] = VERSION`. When the codec carries an
|
|
81
|
+
* `adapterTag`, also sets `options.agents[adapterTag] = VERSION`.
|
|
56
82
|
* Idempotent — repeated calls with the same client and codec produce the same keys/values.
|
|
57
83
|
* Spec: AIT-CT1a, AIT-CT1a2, AIT-CT1a3, AIT-ST1a, AIT-ST1a2, AIT-ST1a3.
|
|
58
84
|
* @param client - The Ably Realtime client to register on.
|
|
59
|
-
* @param codec - The codec instance
|
|
85
|
+
* @param codec - The codec instance whose optional identifier opts into registration.
|
|
86
|
+
* @param codec.adapterTag - The optional Ably-Agent identifier; registered as an agent when present.
|
|
60
87
|
* @returns Channel options containing `params.agent` for `channels.get`.
|
|
61
88
|
*/
|
|
62
|
-
export const registerAgent = (
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (adapterTag) agents[adapterTag] = VERSION;
|
|
67
|
-
return injectAgents(client, agents);
|
|
68
|
-
};
|
|
89
|
+
export const registerAgent = (
|
|
90
|
+
client: Ably.Realtime,
|
|
91
|
+
codec?: { readonly adapterTag?: string },
|
|
92
|
+
): { params: { agent: string } } => injectAgents(client, buildAgents(codec));
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel-mode resolution shared by the sessions and the React provider.
|
|
3
|
+
*
|
|
4
|
+
* Presence, pub/sub, and annotation publishing are all part of the server's
|
|
5
|
+
* default channel-mode set, so a channel attached with no mode flags is granted
|
|
6
|
+
* them automatically. LiveObjects is different: object operations require the
|
|
7
|
+
* `object_subscribe` / `object_publish` modes, which are NOT in the default
|
|
8
|
+
* set, so the channel must be attached with explicit modes to use them.
|
|
9
|
+
*
|
|
10
|
+
* Setting `modes` on the wire is a full replacement, not additive: the moment
|
|
11
|
+
* an ATTACH carries any mode flag the server takes that bitfield verbatim and
|
|
12
|
+
* never falls back to its default set. So requesting object modes means also
|
|
13
|
+
* requesting everything the default set would grant, plus the object modes.
|
|
14
|
+
* {@link AIT_BASE_MODES} is exactly the server default, so opting into extra
|
|
15
|
+
* modes adds the extras and changes nothing else.
|
|
16
|
+
*
|
|
17
|
+
* Every place that resolves the session's channel options — both session
|
|
18
|
+
* constructors and the React `<ChannelProvider>` — must funnel through
|
|
19
|
+
* {@link resolveChannelModes} so they all request the SAME modes in the SAME
|
|
20
|
+
* order. ably-js compares modes order- and duplicate-sensitively when deciding
|
|
21
|
+
* whether a `setOptions` call needs a reattach; identical arrays compare equal,
|
|
22
|
+
* so consistent resolution avoids both spurious reattaches and silent mode
|
|
23
|
+
* reversion when one writer omits modes another set.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type * as Ably from 'ably';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The modes AI Transport always needs — byte-for-byte the server's default
|
|
30
|
+
* channel-mode set (`PUBLISH | SUBSCRIBE | PRESENCE | PRESENCE_SUBSCRIBE |
|
|
31
|
+
* ANNOTATION_PUBLISH`). Because this equals the default, requesting it plus
|
|
32
|
+
* extra modes (e.g. {@link OBJECT_MODES}) grants the same access as the default
|
|
33
|
+
* set plus those extras.
|
|
34
|
+
*/
|
|
35
|
+
export const AIT_BASE_MODES: readonly Ably.ChannelMode[] = [
|
|
36
|
+
'PUBLISH',
|
|
37
|
+
'SUBSCRIBE',
|
|
38
|
+
'PRESENCE',
|
|
39
|
+
'PRESENCE_SUBSCRIBE',
|
|
40
|
+
'ANNOTATION_PUBLISH',
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The channel modes required to read and write Ably LiveObjects. Pass as the
|
|
45
|
+
* session's `channelModes` option (`channelModes: OBJECT_MODES`) to enable the
|
|
46
|
+
* `object` accessor on a session and the LiveObjects channel hooks under a
|
|
47
|
+
* `<ClientSessionProvider>`.
|
|
48
|
+
*/
|
|
49
|
+
export const OBJECT_MODES: readonly Ably.ChannelMode[] = ['OBJECT_SUBSCRIBE', 'OBJECT_PUBLISH'];
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Canonical ordering for every known channel mode. {@link resolveChannelModes}
|
|
53
|
+
* emits modes in this order so two resolutions of the same mode set produce
|
|
54
|
+
* identical arrays, which ably-js treats as equal (no reattach churn).
|
|
55
|
+
*/
|
|
56
|
+
const MODE_ORDER: readonly Ably.ChannelMode[] = [
|
|
57
|
+
'PUBLISH',
|
|
58
|
+
'SUBSCRIBE',
|
|
59
|
+
'PRESENCE',
|
|
60
|
+
'PRESENCE_SUBSCRIBE',
|
|
61
|
+
'OBJECT_PUBLISH',
|
|
62
|
+
'OBJECT_SUBSCRIBE',
|
|
63
|
+
'ANNOTATION_PUBLISH',
|
|
64
|
+
'ANNOTATION_SUBSCRIBE',
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Resolve the channel modes AI Transport should request on its channel.
|
|
69
|
+
*
|
|
70
|
+
* Returns `undefined` when the caller asks for no extra modes, so the channel
|
|
71
|
+
* attaches with no mode flags and the server applies its default set. When
|
|
72
|
+
* extra modes are supplied (e.g. {@link OBJECT_MODES}),
|
|
73
|
+
* returns {@link AIT_BASE_MODES} unioned with them, de-duplicated and in a
|
|
74
|
+
* fixed canonical order so repeated resolutions compare equal.
|
|
75
|
+
*
|
|
76
|
+
* Modes outside {@link MODE_ORDER} (which should not occur for a valid
|
|
77
|
+
* {@link Ably.ChannelMode}, but is possible with the type's lowercase aliases)
|
|
78
|
+
* are appended after the canonical modes, sorted alphabetically, so the result
|
|
79
|
+
* is still deterministic.
|
|
80
|
+
* @param extraModes - Additional modes to request on top of {@link AIT_BASE_MODES}. Omit or pass an empty array to request no modes at all.
|
|
81
|
+
* @returns The canonically-ordered, de-duplicated mode set, or `undefined` when no extra modes were requested.
|
|
82
|
+
*/
|
|
83
|
+
export const resolveChannelModes = (extraModes?: readonly Ably.ChannelMode[]): Ably.ChannelMode[] | undefined => {
|
|
84
|
+
if (extraModes === undefined || extraModes.length === 0) return undefined;
|
|
85
|
+
const requested = new Set<Ably.ChannelMode>([...AIT_BASE_MODES, ...extraModes]);
|
|
86
|
+
const ordered = MODE_ORDER.filter((mode) => requested.has(mode));
|
|
87
|
+
const unknown = [...requested].filter((mode) => !MODE_ORDER.includes(mode)).toSorted();
|
|
88
|
+
return [...ordered, ...unknown];
|
|
89
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `toCodecEvents` — tag a decoded message's events with their wire direction.
|
|
3
|
+
*
|
|
4
|
+
* A decoded message is already split into inputs and outputs by the decoder
|
|
5
|
+
* (driven by the Ably message name — the authoritative direction signal). This
|
|
6
|
+
* helper folds that split into the ordered {@link CodecEvent} stream the reducer
|
|
7
|
+
* consumes, so the direction is carried explicitly rather than re-inferred from
|
|
8
|
+
* each event's shape. Inputs are tagged before outputs, preserving the wire
|
|
9
|
+
* order within a single message (a message is single-direction, so the relative
|
|
10
|
+
* order of the two groups is immaterial).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { CodecEvent, CodecInputEvent, CodecOutputEvent, DecodedMessage } from './types.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Tag a decoded message's events with their wire direction.
|
|
17
|
+
* @template TInput - The codec's input union.
|
|
18
|
+
* @template TOutput - The codec's output union.
|
|
19
|
+
* @param decoded - The decoder's input/output split for one inbound message.
|
|
20
|
+
* @returns The events as a direction-tagged {@link CodecEvent} list, inputs first.
|
|
21
|
+
*/
|
|
22
|
+
export const toCodecEvents = <TInput extends CodecInputEvent, TOutput extends CodecOutputEvent>(
|
|
23
|
+
decoded: DecodedMessage<TInput, TOutput>,
|
|
24
|
+
): CodecEvent<TInput, TOutput>[] => [
|
|
25
|
+
...decoded.inputs.map((event): CodecEvent<TInput, TOutput> => ({ direction: 'input', event })),
|
|
26
|
+
...decoded.outputs.map((event): CodecEvent<TInput, TOutput> => ({ direction: 'output', event })),
|
|
27
|
+
];
|