@ably/ai-transport 0.1.0 → 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.
Files changed (163) hide show
  1. package/README.md +91 -100
  2. package/dist/ably-ai-transport.js +1553 -1238
  3. package/dist/ably-ai-transport.js.map +1 -1
  4. package/dist/ably-ai-transport.umd.cjs +1 -1
  5. package/dist/ably-ai-transport.umd.cjs.map +1 -1
  6. package/dist/constants.d.ts +116 -42
  7. package/dist/core/agent.d.ts +29 -0
  8. package/dist/core/codec/decoder.d.ts +20 -23
  9. package/dist/core/codec/encoder.d.ts +11 -8
  10. package/dist/core/codec/index.d.ts +1 -2
  11. package/dist/core/codec/lifecycle-tracker.d.ts +10 -9
  12. package/dist/core/codec/types.d.ts +407 -115
  13. package/dist/core/transport/agent-session.d.ts +10 -0
  14. package/dist/core/transport/branch-chain.d.ts +43 -0
  15. package/dist/core/transport/client-session.d.ts +13 -0
  16. package/dist/core/transport/decode-fold.d.ts +47 -0
  17. package/dist/core/transport/headers.d.ts +96 -18
  18. package/dist/core/transport/index.d.ts +5 -6
  19. package/dist/core/transport/internal/bounded-map.d.ts +20 -0
  20. package/dist/core/transport/invocation.d.ts +74 -0
  21. package/dist/core/transport/load-conversation.d.ts +128 -0
  22. package/dist/core/transport/load-history.d.ts +39 -0
  23. package/dist/core/transport/pipe-stream.d.ts +9 -9
  24. package/dist/core/transport/run-manager.d.ts +78 -0
  25. package/dist/core/transport/tree.d.ts +373 -109
  26. package/dist/core/transport/types/agent.d.ts +353 -0
  27. package/dist/core/transport/types/client.d.ts +168 -0
  28. package/dist/core/transport/types/shared.d.ts +24 -0
  29. package/dist/core/transport/types/tree.d.ts +315 -0
  30. package/dist/core/transport/types/view.d.ts +222 -0
  31. package/dist/core/transport/types.d.ts +13 -553
  32. package/dist/core/transport/view.d.ts +272 -84
  33. package/dist/errors.d.ts +21 -10
  34. package/dist/index.d.ts +6 -8
  35. package/dist/logger.d.ts +12 -0
  36. package/dist/react/ably-ai-transport-react.js +976 -990
  37. package/dist/react/ably-ai-transport-react.js.map +1 -1
  38. package/dist/react/ably-ai-transport-react.umd.cjs +1 -1
  39. package/dist/react/ably-ai-transport-react.umd.cjs.map +1 -1
  40. package/dist/react/contexts/client-session-context.d.ts +36 -0
  41. package/dist/react/contexts/client-session-provider.d.ts +53 -0
  42. package/dist/react/create-session-hooks.d.ts +116 -0
  43. package/dist/react/index.d.ts +12 -12
  44. package/dist/react/internal/use-resolved-session.d.ts +36 -0
  45. package/dist/react/use-ably-messages.d.ts +17 -14
  46. package/dist/react/use-client-session.d.ts +81 -0
  47. package/dist/react/use-create-view.d.ts +14 -13
  48. package/dist/react/use-tree.d.ts +30 -15
  49. package/dist/react/use-view.d.ts +82 -51
  50. package/dist/utils.d.ts +32 -23
  51. package/dist/vercel/ably-ai-transport-vercel.js +2573 -2086
  52. package/dist/vercel/ably-ai-transport-vercel.js.map +1 -1
  53. package/dist/vercel/ably-ai-transport-vercel.umd.cjs +1 -1
  54. package/dist/vercel/ably-ai-transport-vercel.umd.cjs.map +1 -1
  55. package/dist/vercel/codec/decoder.d.ts +5 -18
  56. package/dist/vercel/codec/encoder.d.ts +6 -36
  57. package/dist/vercel/codec/events.d.ts +51 -0
  58. package/dist/vercel/codec/index.d.ts +24 -12
  59. package/dist/vercel/codec/reducer.d.ts +144 -0
  60. package/dist/vercel/codec/tool-transitions.d.ts +2 -2
  61. package/dist/vercel/index.d.ts +4 -5
  62. package/dist/vercel/react/ably-ai-transport-vercel-react.js +3907 -3266
  63. package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
  64. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +33 -8
  65. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
  66. package/dist/vercel/react/contexts/chat-transport-context.d.ts +7 -6
  67. package/dist/vercel/react/contexts/chat-transport-provider.d.ts +53 -41
  68. package/dist/vercel/react/index.d.ts +1 -2
  69. package/dist/vercel/react/use-chat-transport.d.ts +30 -26
  70. package/dist/vercel/react/use-message-sync.d.ts +17 -30
  71. package/dist/vercel/run-end-reason.d.ts +29 -0
  72. package/dist/vercel/transport/chat-transport.d.ts +43 -24
  73. package/dist/vercel/transport/index.d.ts +25 -21
  74. package/dist/vercel/transport/run-output-stream.d.ts +56 -0
  75. package/dist/version.d.ts +2 -0
  76. package/package.json +30 -23
  77. package/src/constants.ts +124 -51
  78. package/src/core/agent.ts +68 -0
  79. package/src/core/codec/decoder.ts +71 -98
  80. package/src/core/codec/encoder.ts +113 -65
  81. package/src/core/codec/index.ts +13 -6
  82. package/src/core/codec/lifecycle-tracker.ts +10 -9
  83. package/src/core/codec/types.ts +436 -120
  84. package/src/core/transport/agent-session.ts +1344 -0
  85. package/src/core/transport/branch-chain.ts +58 -0
  86. package/src/core/transport/client-session.ts +775 -0
  87. package/src/core/transport/decode-fold.ts +91 -0
  88. package/src/core/transport/headers.ts +181 -22
  89. package/src/core/transport/index.ts +25 -26
  90. package/src/core/transport/internal/bounded-map.ts +27 -0
  91. package/src/core/transport/invocation.ts +98 -0
  92. package/src/core/transport/load-conversation.ts +355 -0
  93. package/src/core/transport/load-history.ts +269 -0
  94. package/src/core/transport/pipe-stream.ts +54 -39
  95. package/src/core/transport/run-manager.ts +249 -0
  96. package/src/core/transport/tree.ts +926 -308
  97. package/src/core/transport/types/agent.ts +407 -0
  98. package/src/core/transport/types/client.ts +211 -0
  99. package/src/core/transport/types/shared.ts +27 -0
  100. package/src/core/transport/types/tree.ts +344 -0
  101. package/src/core/transport/types/view.ts +259 -0
  102. package/src/core/transport/types.ts +13 -706
  103. package/src/core/transport/view.ts +864 -433
  104. package/src/errors.ts +22 -9
  105. package/src/event-emitter.ts +3 -2
  106. package/src/index.ts +52 -41
  107. package/src/logger.ts +14 -1
  108. package/src/react/contexts/client-session-context.ts +41 -0
  109. package/src/react/contexts/client-session-provider.tsx +186 -0
  110. package/src/react/create-session-hooks.ts +141 -0
  111. package/src/react/index.ts +23 -13
  112. package/src/react/internal/use-resolved-session.ts +63 -0
  113. package/src/react/use-ably-messages.ts +32 -22
  114. package/src/react/use-client-session.ts +201 -0
  115. package/src/react/use-create-view.ts +33 -29
  116. package/src/react/use-tree.ts +61 -30
  117. package/src/react/use-view.ts +139 -97
  118. package/src/utils.ts +63 -45
  119. package/src/vercel/codec/decoder.ts +336 -258
  120. package/src/vercel/codec/encoder.ts +343 -205
  121. package/src/vercel/codec/events.ts +87 -0
  122. package/src/vercel/codec/index.ts +60 -13
  123. package/src/vercel/codec/reducer.ts +977 -0
  124. package/src/vercel/codec/tool-transitions.ts +2 -2
  125. package/src/vercel/index.ts +6 -19
  126. package/src/vercel/react/contexts/chat-transport-context.ts +7 -6
  127. package/src/vercel/react/contexts/chat-transport-provider.tsx +87 -59
  128. package/src/vercel/react/index.ts +3 -5
  129. package/src/vercel/react/use-chat-transport.ts +47 -49
  130. package/src/vercel/react/use-message-sync.ts +80 -39
  131. package/src/vercel/run-end-reason.ts +78 -0
  132. package/src/vercel/transport/chat-transport.ts +392 -98
  133. package/src/vercel/transport/index.ts +39 -38
  134. package/src/vercel/transport/run-output-stream.ts +170 -0
  135. package/src/version.ts +2 -0
  136. package/dist/core/transport/client-transport.d.ts +0 -10
  137. package/dist/core/transport/decode-history.d.ts +0 -43
  138. package/dist/core/transport/server-transport.d.ts +0 -7
  139. package/dist/core/transport/stream-router.d.ts +0 -29
  140. package/dist/core/transport/turn-manager.d.ts +0 -37
  141. package/dist/react/contexts/transport-context.d.ts +0 -31
  142. package/dist/react/contexts/transport-provider.d.ts +0 -49
  143. package/dist/react/create-transport-hooks.d.ts +0 -124
  144. package/dist/react/use-active-turns.d.ts +0 -12
  145. package/dist/react/use-client-transport.d.ts +0 -80
  146. package/dist/vercel/codec/accumulator.d.ts +0 -21
  147. package/dist/vercel/react/use-staged-add-tool-approval-response.d.ts +0 -30
  148. package/dist/vercel/tool-approvals.d.ts +0 -124
  149. package/dist/vercel/tool-events.d.ts +0 -26
  150. package/src/core/transport/client-transport.ts +0 -977
  151. package/src/core/transport/decode-history.ts +0 -485
  152. package/src/core/transport/server-transport.ts +0 -612
  153. package/src/core/transport/stream-router.ts +0 -136
  154. package/src/core/transport/turn-manager.ts +0 -165
  155. package/src/react/contexts/transport-context.ts +0 -37
  156. package/src/react/contexts/transport-provider.tsx +0 -164
  157. package/src/react/create-transport-hooks.ts +0 -144
  158. package/src/react/use-active-turns.ts +0 -72
  159. package/src/react/use-client-transport.ts +0 -197
  160. package/src/vercel/codec/accumulator.ts +0 -588
  161. package/src/vercel/react/use-staged-add-tool-approval-response.ts +0 -87
  162. package/src/vercel/tool-approvals.ts +0 -380
  163. package/src/vercel/tool-events.ts +0 -53
@@ -0,0 +1,56 @@
1
+ import { ClientSession } from '../../core/transport/types.js';
2
+ import { VercelInput, VercelOutput, VercelProjection } from '../codec/index.js';
3
+ /**
4
+ * Vercel-owned per-run output stream.
5
+ *
6
+ * Builds the `ReadableStream<UIMessageChunk>` that `useChat` consumes by
7
+ * subscribing to the session Tree's `output` and `run` events for a single
8
+ * run. Streaming is a useChat-integration concern, so it lives in the Vercel
9
+ * layer rather than the generic core: the core Tree is the fan-out point, and
10
+ * this projects its events into the shape `useChat` expects.
11
+ *
12
+ * Close semantics — the stream the consumer reads ends when:
13
+ * - a **terminal chunk** (`finish` / `error` / `abort`) is folded for the run.
14
+ * This is the signal `useChat`'s `sendAutomaticallyWhen` waits for, and it
15
+ * fires even when the run merely *suspends* for a tool call (a tool-calls
16
+ * `finish` ends the consumer stream while the core run stays alive in the
17
+ * Tree for the continuation); or
18
+ * - the run reaches `run-end`, which is always terminal (safety net for a run
19
+ * that ends without emitting a terminal chunk). A `run-suspend` keeps the
20
+ * core run alive and does not close the consumer stream.
21
+ *
22
+ * It errors when the session emits a non-fatal `error` (e.g. channel
23
+ * continuity loss, or an agent-reported mid-run error), so the consumer's
24
+ * reader rejects rather than hanging.
25
+ */
26
+ import * as Ably from 'ably';
27
+ import type * as AI from 'ai';
28
+ type VercelSession = ClientSession<VercelInput, VercelOutput, VercelProjection, AI.UIMessage>;
29
+ /** A consumer-facing run output stream plus the handles to settle it externally. */
30
+ export interface RunOutputStream {
31
+ /** The stream of decoded outputs for the run, as `useChat` consumes it. */
32
+ stream: ReadableStream<VercelOutput>;
33
+ /** Close the stream now (e.g. on local cancel). Idempotent. */
34
+ close: () => void;
35
+ /** Error the stream now (e.g. on a failed agent-invocation POST). Idempotent. */
36
+ error: (reason: Ably.ErrorInfo) => void;
37
+ }
38
+ /**
39
+ * Create a consumer-facing output stream for a send, sourced from the session
40
+ * Tree's events. See the module docs for close/error semantics. The returned
41
+ * `close`/`error` let the caller settle the stream for conditions the Tree
42
+ * doesn't surface (local cancel, POST failure).
43
+ *
44
+ * Outputs route PURELY by the triggering input's codec-message-id — the key the
45
+ * client owns from send time, before the agent mints the runId. The agent's
46
+ * minted runId is supplied as a promise so the run-end safety-net can still
47
+ * close the stream once it resolves.
48
+ * @param session - The Vercel client session whose Tree to observe.
49
+ * @param runId - The agent-minted runId, resolved when run-start is observed.
50
+ * Used only by the run-end safety-net; routing keys on `inputCodecMessageId`.
51
+ * @param inputCodecMessageId - The triggering input's codec-message-id. An
52
+ * output routes to this stream when it carries this id.
53
+ * @returns The stream and its external settle handles.
54
+ */
55
+ export declare const createRunOutputStream: (session: VercelSession, runId: Promise<string>, inputCodecMessageId: string) => RunOutputStream;
56
+ export {};
@@ -0,0 +1,2 @@
1
+ /** SDK version. Kept in sync with `package.json` by the `/release` workflow. */
2
+ export declare const VERSION = "0.2.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ably/ai-transport",
3
- "version": "0.1.0",
3
+ "version": "0.2.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",
@@ -36,26 +36,6 @@
36
36
  },
37
37
  "./package.json": "./package.json"
38
38
  },
39
- "scripts": {
40
- "build": "npm run build:clean && npm run build:core && npm run build:react && npm run build:vercel && npm run build:vercel-react",
41
- "build:clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
42
- "build:core": "vite build --config ./src/vite.config.ts",
43
- "build:react": "vite build --config ./src/react/vite.config.ts --emptyOutDir",
44
- "build:vercel": "vite build --config ./src/vercel/vite.config.ts --emptyOutDir",
45
- "build:vercel-react": "vite build --config ./src/vercel/react/vite.config.ts --emptyOutDir",
46
- "prepare": "npm run build",
47
- "lint": "eslint .",
48
- "lint:fix": "eslint --fix .; (npm run format > /dev/null)",
49
- "format": "prettier --list-different --write .",
50
- "format:check": "prettier --check .",
51
- "typecheck": "tsc --noEmit",
52
- "test": "vitest run",
53
- "test:integration": "vitest run --config vitest.config.integration.ts",
54
- "check:error-codes": "tsx scripts/validate-error-codes.ts",
55
- "precommit": "npm run format:check && npm run lint && npm run typecheck",
56
- "docs": "typedoc",
57
- "docs:lint": "typedoc --emit none"
58
- },
59
39
  "files": [
60
40
  "dist/**",
61
41
  "src/**",
@@ -80,7 +60,14 @@
80
60
  },
81
61
  "homepage": "https://github.com/ably/ably-ai-transport-js#readme",
82
62
  "engines": {
83
- "node": ">=20.0.0"
63
+ "node": ">=22.0.0"
64
+ },
65
+ "devEngines": {
66
+ "packageManager": {
67
+ "name": "pnpm",
68
+ "version": "11.3.0",
69
+ "onFail": "download"
70
+ }
84
71
  },
85
72
  "peerDependencies": {
86
73
  "ably": "^2.21.0",
@@ -101,6 +88,7 @@
101
88
  "@eslint/eslintrc": "^3.3.0",
102
89
  "@eslint/js": "^10.0.1",
103
90
  "@testing-library/react": "^16.3.2",
91
+ "@types/node": "^25.0.0",
104
92
  "@types/react": "^19.2.14",
105
93
  "@typescript-eslint/eslint-plugin": "^8.58.2",
106
94
  "@typescript-eslint/parser": "^8.58.2",
@@ -125,5 +113,24 @@
125
113
  "vite": "^8.0.0",
126
114
  "vite-plugin-dts": "^4.5.4",
127
115
  "vitest": "^4.1.0"
116
+ },
117
+ "scripts": {
118
+ "build": "pnpm run build:clean && pnpm run build:core && pnpm run build:react && pnpm run build:vercel && pnpm run build:vercel-react",
119
+ "build:clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
120
+ "build:core": "vite build --config ./src/vite.config.ts",
121
+ "build:react": "vite build --config ./src/react/vite.config.ts --emptyOutDir",
122
+ "build:vercel": "vite build --config ./src/vercel/vite.config.ts --emptyOutDir",
123
+ "build:vercel-react": "vite build --config ./src/vercel/react/vite.config.ts --emptyOutDir",
124
+ "lint": "eslint .",
125
+ "lint:fix": "eslint --fix .; (pnpm run format > /dev/null)",
126
+ "format": "prettier --list-different --write .",
127
+ "format:check": "prettier --check .",
128
+ "typecheck": "tsc --noEmit",
129
+ "test": "vitest run",
130
+ "test:integration": "vitest run --config vitest.config.integration.ts",
131
+ "check:error-codes": "tsx scripts/validate-error-codes.ts",
132
+ "precommit": "pnpm run format:check && pnpm run lint && pnpm run typecheck",
133
+ "docs": "typedoc",
134
+ "docs:lint": "typedoc --emit none"
128
135
  }
129
- }
136
+ }
package/src/constants.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * Shared constants used by both codec and transport layers.
3
3
  *
4
- * Header constants define the `x-ably-*` wire protocol. Message and event
5
- * name constants define the transport lifecycle signals on the channel.
4
+ * Header constants define the transport wire header names. Message and event
5
+ * name constants define the session lifecycle signals on the channel.
6
6
  *
7
7
  * These live at the top level (not in codec/ or transport/) because both
8
8
  * layers need them — the codec core reads/writes stream and status headers,
9
- * while the transport layer reads/writes turn, cancel, and role headers.
9
+ * while the transport layer reads/writes run, cancel, and role headers.
10
10
  */
11
11
 
12
12
  // ---------------------------------------------------------------------------
@@ -14,91 +14,164 @@
14
14
  // ---------------------------------------------------------------------------
15
15
 
16
16
  /** Header: whether this Ably message uses streaming (message appends) or is discrete. Always "true" or "false". */
17
- export const HEADER_STREAM = 'x-ably-stream';
17
+ export const HEADER_STREAM = 'stream';
18
18
 
19
- /** Header: lifecycle status of a streamed message. Only set when x-ably-stream is "true". */
20
- export const HEADER_STATUS = 'x-ably-status';
19
+ /** Header: lifecycle status of a streamed message. Only set when stream is "true". One of "streaming", "complete", or "cancelled". */
20
+ export const HEADER_STATUS = 'status';
21
21
 
22
22
  /** Header: stream identity. Set by the encoder on every streamed message; read by the decoder to correlate streams. */
23
- export const HEADER_STREAM_ID = 'x-ably-stream-id';
23
+ export const HEADER_STREAM_ID = 'stream-id';
24
24
 
25
25
  /** Header: marks a message as a discrete message part (from writeMessages). Set by publishDiscreteBatch; not set on lifecycle events from publishDiscrete. */
26
- export const HEADER_DISCRETE = 'x-ably-discrete';
26
+ export const HEADER_DISCRETE = 'discrete';
27
27
 
28
28
  // ---------------------------------------------------------------------------
29
- // Identity headers (used by transport for turn correlation)
29
+ // Identity headers (used by transport for run correlation)
30
30
  // ---------------------------------------------------------------------------
31
31
 
32
- /** Header: turn correlation ID. Set on every message in a turn. */
33
- export const HEADER_TURN_ID = 'x-ably-turn-id';
32
+ /** Header: run correlation ID. Set on every agent-published message and on continuation client inputs, but omitted from the originating fresh client input (the agent mints the run-id at run-start). */
33
+ export const HEADER_RUN_ID = 'run-id';
34
+
35
+ /** Header: invocation correlation ID; identifies a specific invocation under a run. Agent-minted and stamped by the agent on every event it publishes for the invocation — run lifecycle (run-start/resume/suspend/end) and assistant outputs. Never set by the client on its input. */
36
+ export const HEADER_INVOCATION_ID = 'invocation-id';
37
+
38
+ /**
39
+ * Header: per-event identifier stamped by the client on every
40
+ * client-published event in a send — user-message events AND amend
41
+ * events (tool-approval responses, client tool outputs). Distinct from
42
+ * `codec-message-id` so it survives edits/retries that reuse the same
43
+ * codec-message-id, and so amend events that target an existing message can
44
+ * carry their own per-send identity. The invocation body lists every
45
+ * inputEventId the agent must observe on the channel before starting LLM
46
+ * work — see `Run.start()`'s input-event lookup.
47
+ */
48
+ export const HEADER_EVENT_ID = 'event-id';
34
49
 
35
50
  /** Header: message identity. Assigned per message (user or assistant). Used for optimistic reconciliation on the client. */
36
- export const HEADER_MSG_ID = 'x-ably-msg-id';
51
+ export const HEADER_CODEC_MESSAGE_ID = 'codec-message-id';
37
52
 
38
- /** Header: clientId of the user who initiated the turn. Set by the server on stream messages. */
39
- export const HEADER_TURN_CLIENT_ID = 'x-ably-turn-client-id';
53
+ /** Header: clientId of the user who initiated the run. Stamped by the client on its user input and re-stamped by the agent on the run's lifecycle and stream messages. */
54
+ export const HEADER_RUN_CLIENT_ID = 'run-client-id';
40
55
 
41
- /** Header: message role (e.g. "user", "assistant"). */
42
- export const HEADER_ROLE = 'x-ably-role';
56
+ /**
57
+ * Header: clientId of the input event (the `ai-input`) that drove the
58
+ * current invocation. The agent reads the publisher's Ably-level `clientId`
59
+ * from the triggering input event on the channel and re-stamps it as
60
+ * `input-client-id` on every event it publishes for that invocation
61
+ * (run lifecycle and assistant outputs). May differ from
62
+ * `run-client-id` on continuation invocations driven by an input
63
+ * from a non-owner (e.g. a tool-result publish from a different client).
64
+ * Not stamped on `ai-input` events themselves — the wire publisher's
65
+ * Ably `clientId` already conveys that.
66
+ */
67
+ export const HEADER_INPUT_CLIENT_ID = 'input-client-id';
43
68
 
44
- /** Header: the msg-id of the existing message this Ably message amends. Present on cross-turn amendment events. */
45
- export const HEADER_AMEND = 'x-ably-amend';
69
+ /** Header: message role (e.g. "user", "assistant"). */
70
+ export const HEADER_ROLE = 'role';
46
71
 
47
72
  // ---------------------------------------------------------------------------
48
- // Cancel headers
73
+ // Fork / branching headers
49
74
  // ---------------------------------------------------------------------------
50
75
 
51
- /** Header: cancel a specific turn by ID. */
52
- export const HEADER_CANCEL_TURN_ID = 'x-ably-cancel-turn-id';
53
-
54
- /** Header: cancel all turns belonging to the sender's clientId. */
55
- export const HEADER_CANCEL_OWN = 'x-ably-cancel-own';
76
+ /** Header: the codec-message-id of the immediately preceding message in this branch. */
77
+ export const HEADER_PARENT = 'parent';
56
78
 
57
- /** Header: cancel all turns on the channel. */
58
- export const HEADER_CANCEL_ALL = 'x-ably-cancel-all';
79
+ /** Header: the codec-message-id of the message this one replaces (creates a fork). */
80
+ export const HEADER_FORK_OF = 'fork-of';
59
81
 
60
- /** Header: cancel all turns belonging to a specific clientId. */
61
- export const HEADER_CANCEL_CLIENT_ID = 'x-ably-cancel-client-id';
82
+ /**
83
+ * Header: the codec-message-id of the assistant message this run regenerates.
84
+ *
85
+ * Stamped on the regenerate wire (and echoed on `run-start`) when the
86
+ * client requested a regeneration. A regenerate run parents at the SAME input
87
+ * node as the reply it regenerates, so it joins that input's reply runs as a
88
+ * same-parent sibling (no fork-of). The View consults this header to resolve
89
+ * the message-level sibling group and to drop the regenerated message from
90
+ * earlier Runs in the visible chain (Spec: AIT-CT13d).
91
+ */
92
+ export const HEADER_MSG_REGENERATE = 'msg-regenerate';
62
93
 
63
94
  // ---------------------------------------------------------------------------
64
- // Fork / branching headers
95
+ // Run lifecycle headers
65
96
  // ---------------------------------------------------------------------------
66
97
 
67
- /** Header: the msg-id of the immediately preceding message in this branch. */
68
- export const HEADER_PARENT = 'x-ably-parent';
98
+ /** Header: reason a run ended (on ai-run-end messages). */
99
+ export const HEADER_RUN_REASON = 'run-reason';
69
100
 
70
- /** Header: the msg-id of the message this one replaces (creates a fork). */
71
- export const HEADER_FORK_OF = 'x-ably-fork-of';
101
+ /**
102
+ * Header: the `codec-message-id` of the input event that triggered the run.
103
+ * The triggering input is the one whose `event-id` matches the invocation's
104
+ * `inputEventId` (the last input of the originating send). The agent
105
+ * re-stamps it on every event it publishes for the invocation (run
106
+ * lifecycle + assistant outputs), mirroring `input-client-id`. This is the
107
+ * codec-message-id the client owns at send time, so it lets the client
108
+ * correlate any of those events back to the originating input without
109
+ * depending on a client-minted run-id or invocation-id.
110
+ */
111
+ export const HEADER_INPUT_CODEC_MESSAGE_ID = 'input-codec-message-id';
72
112
 
73
113
  // ---------------------------------------------------------------------------
74
- // Turn lifecycle headers
114
+ // Run-end error headers (set on `ai-run-end` when `run-reason: error`)
75
115
  // ---------------------------------------------------------------------------
76
116
 
77
- /** Header: reason a turn ended (on x-ably-turn-end messages). */
78
- export const HEADER_TURN_REASON = 'x-ably-turn-reason';
117
+ /** Header: numeric error code accompanying an `ai-run-end` with reason `error`. */
118
+ export const HEADER_ERROR_CODE = 'error-code';
119
+
120
+ /** Header: human-readable error message accompanying an `ai-run-end` with reason `error`. */
121
+ export const HEADER_ERROR_MESSAGE = 'error-message';
79
122
 
80
123
  // ---------------------------------------------------------------------------
81
124
  // Message / event names
82
125
  // ---------------------------------------------------------------------------
83
126
 
84
- /** Message name: client->server cancel signal. */
85
- export const EVENT_CANCEL = 'x-ably-cancel';
127
+ /**
128
+ * Message name: client->agent cancel intent. Targets a run by `run-id` (a
129
+ * continuation, whose run-id the client already knows) and/or by
130
+ * `input-codec-message-id` (a fresh send, whose run-id the agent mints at
131
+ * run-start — so the client can only key the cancel by the triggering input's
132
+ * codec-message-id it owns at send time). The agent resolves whichever is
133
+ * present to the registered run; a cancel that arrives before the run is known
134
+ * (the input-event lookup hasn't resolved the input id to a run yet) is
135
+ * buffered by `input-codec-message-id` and honoured when the run resolves it.
136
+ * Also carries an `event-id` so channel rewind redelivers it to a per-request /
137
+ * serverless agent that attaches after the cancel was published.
138
+ */
139
+ export const EVENT_CANCEL = 'ai-cancel';
86
140
 
87
- /** Message name: server publishes this to signal a turn has started. */
88
- export const EVENT_TURN_START = 'x-ably-turn-start';
141
+ /** Message name: server publishes this to signal a run has started. */
142
+ export const EVENT_RUN_START = 'ai-run-start';
89
143
 
90
- /** Message name: server publishes this to signal a turn has ended. */
91
- export const EVENT_TURN_END = 'x-ably-turn-end';
144
+ /**
145
+ * Message name: server publishes this to signal a run has suspended — paused
146
+ * awaiting participant input (e.g. a client tool result or approval) without
147
+ * ending. The run stays live and may be resumed under the same `runId`.
148
+ * Distinct from `ai-run-end`, which is terminal.
149
+ */
150
+ export const EVENT_RUN_SUSPEND = 'ai-run-suspend';
92
151
 
93
- /** Message name: transport-level abort signal (stream cancelled). */
94
- export const EVENT_ABORT = 'x-ably-abort';
152
+ /**
153
+ * Message name: server publishes this when a subsequent invocation re-enters an
154
+ * already-started run (e.g. a tool-result follow-up under the same `runId`).
155
+ * A pure re-entry signal: unlike `ai-run-start` it carries no `parent` / `fork-of`
156
+ * (the original `ai-run-start` already established the run's structure).
157
+ */
158
+ export const EVENT_RUN_RESUME = 'ai-run-resume';
95
159
 
96
- /** Message name: transport-level error signal. */
97
- export const EVENT_ERROR = 'x-ably-error';
160
+ /** Message name: server publishes this to signal a run has ended. */
161
+ export const EVENT_RUN_END = 'ai-run-end';
98
162
 
99
- // ---------------------------------------------------------------------------
100
- // Domain header prefix (used by codec implementations)
101
- // ---------------------------------------------------------------------------
163
+ /**
164
+ * Message name: every agent-published codec event (text, reasoning, tool calls,
165
+ * tool outputs, lifecycle helpers, file / source parts, data-* chunks) rides
166
+ * this single wire name. The codec event's own `type` is carried in the
167
+ * codec-level `type` header so the decoder can dispatch.
168
+ */
169
+ export const EVENT_AI_OUTPUT = 'ai-output';
102
170
 
103
- /** Prefix for domain-specific headers. Distinguishes codec-layer headers from transport `x-ably-*` headers. */
104
- export const DOMAIN_HEADER_PREFIX = 'x-domain-';
171
+ /**
172
+ * Message name: every client-published codec event (user-message parts,
173
+ * tool-approval responses, regenerate signals) rides this single wire
174
+ * name. The codec event's own kind is carried in the codec-level `type`
175
+ * header so the decoder can dispatch.
176
+ */
177
+ export const EVENT_AI_INPUT = 'ai-input';
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Wraps the two paths chat-js uses (see ChatClient._addAgent): the
3
+ * `options.agents` mutation (read by ably-js when opening the initial
4
+ * WebSocket) and the `params.agent` channel option (sent on ATTACH so
5
+ * an already-open connection still carries the identifier).
6
+ *
7
+ * `options.agents` is a private API on the Realtime client — no public
8
+ * typed accessor exists in the `ably` package — so this module casts to a
9
+ * `RealtimeWithOptions` shape to write it.
10
+ */
11
+
12
+ import type * as Ably from 'ably';
13
+
14
+ import { VERSION } from '../version.js';
15
+
16
+ interface RealtimeWithOptions extends Ably.Realtime {
17
+ options: { agents?: Record<string, string | undefined> };
18
+ }
19
+
20
+ const SDK_NAME = 'ai-transport-js';
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
+ /**
28
+ * Merge `agents` into `client.options.agents` and return the space-separated
29
+ * `params.agent` string for channel ATTACH.
30
+ * @param client - The Ably Realtime client to mutate.
31
+ * @param agents - Map of agent-name to version strings to register.
32
+ * @returns Channel options containing `params.agent` for `channels.get`.
33
+ */
34
+ const injectAgents = (
35
+ client: Ably.Realtime,
36
+ // CAST: Ably.Realtime's public type omits `options.agents`, but the SDK
37
+ // does carry it at runtime. ably-chat-js relies on the same shape — see
38
+ // ChatClient._addAgent in https://github.com/ably/ably-chat-js.
39
+ agents: Record<string, string>,
40
+ ): { params: { agent: string } } => {
41
+ const realtime = client as RealtimeWithOptions;
42
+ realtime.options.agents = { ...realtime.options.agents, ...agents };
43
+ const agentString = Object.entries(agents)
44
+ .map(([name, version]) => `${name}/${version}`)
45
+ .join(' ');
46
+ return { params: { agent: agentString } };
47
+ };
48
+
49
+ /**
50
+ * Register this SDK (and optionally a codec) on the supplied Realtime client
51
+ * and return the channel options the caller should pass to
52
+ * `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
+ * an internal `adapterTag` field (via {@link AdapterTagHolder}), also sets
55
+ * `options.agents[adapterTag] = VERSION`.
56
+ * Idempotent — repeated calls with the same client and codec produce the same keys/values.
57
+ * Spec: AIT-CT1a, AIT-CT1a2, AIT-CT1a3, AIT-ST1a, AIT-ST1a2, AIT-ST1a3.
58
+ * @param client - The Ably Realtime client to register on.
59
+ * @param codec - The codec instance; cast to {@link AdapterTagHolder} to detect an optional identifier.
60
+ * @returns Channel options containing `params.agent` for `channels.get`.
61
+ */
62
+ export const registerAgent = (client: Ably.Realtime, codec?: unknown): { params: { agent: string } } => {
63
+ // CAST: AdapterTagHolder is an internal opt-in shape — not part of the public Codec interface.
64
+ const adapterTag = (codec as AdapterTagHolder | undefined)?.adapterTag;
65
+ const agents: Record<string, string> = { [SDK_NAME]: VERSION };
66
+ if (adapterTag) agents[adapterTag] = VERSION;
67
+ return injectAgents(client, agents);
68
+ };