@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.
Files changed (166) hide show
  1. package/README.md +10 -19
  2. package/dist/ably-ai-transport.js +1790 -1091
  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 +2 -2
  7. package/dist/core/agent.d.ts +20 -5
  8. package/dist/core/channel-options.d.ts +57 -0
  9. package/dist/core/codec/codec-event.d.ts +9 -0
  10. package/dist/core/codec/decoder.d.ts +4 -1
  11. package/dist/core/codec/define-codec.d.ts +100 -0
  12. package/dist/core/codec/encoder.d.ts +2 -7
  13. package/dist/core/codec/field-bag.d.ts +85 -0
  14. package/dist/core/codec/fields.d.ts +141 -0
  15. package/dist/core/codec/index.d.ts +8 -1
  16. package/dist/core/codec/input-descriptor-decoder.d.ts +19 -0
  17. package/dist/core/codec/input-descriptor-encoder.d.ts +22 -0
  18. package/dist/core/codec/input-descriptors.d.ts +281 -0
  19. package/dist/core/codec/output-descriptor-decoder.d.ts +29 -0
  20. package/dist/core/codec/output-descriptor-encoder.d.ts +31 -0
  21. package/dist/core/codec/output-descriptors.d.ts +237 -0
  22. package/dist/core/codec/types.d.ts +95 -36
  23. package/dist/core/codec/well-known-inputs.d.ts +52 -0
  24. package/dist/core/transport/agent-view.d.ts +296 -0
  25. package/dist/core/transport/decode-fold.d.ts +40 -32
  26. package/dist/core/transport/headers.d.ts +30 -1
  27. package/dist/core/transport/index.d.ts +1 -1
  28. package/dist/core/transport/invocation.d.ts +1 -1
  29. package/dist/core/transport/load-history-pages.d.ts +71 -0
  30. package/dist/core/transport/load-history.d.ts +21 -16
  31. package/dist/core/transport/run-manager.d.ts +9 -11
  32. package/dist/core/transport/session-support.d.ts +55 -0
  33. package/dist/core/transport/tree.d.ts +165 -15
  34. package/dist/core/transport/types/agent.d.ts +120 -98
  35. package/dist/core/transport/types/client.d.ts +45 -12
  36. package/dist/core/transport/types/tree.d.ts +52 -10
  37. package/dist/core/transport/types/view.d.ts +55 -28
  38. package/dist/core/transport/view.d.ts +176 -58
  39. package/dist/core/transport/wire-log.d.ts +102 -0
  40. package/dist/errors.d.ts +10 -4
  41. package/dist/index.d.ts +6 -5
  42. package/dist/react/ably-ai-transport-react.js +784 -415
  43. package/dist/react/ably-ai-transport-react.js.map +1 -1
  44. package/dist/react/ably-ai-transport-react.umd.cjs +1 -1
  45. package/dist/react/ably-ai-transport-react.umd.cjs.map +1 -1
  46. package/dist/react/contexts/client-session-context.d.ts +2 -1
  47. package/dist/react/contexts/client-session-provider.d.ts +3 -0
  48. package/dist/react/index.d.ts +2 -1
  49. package/dist/react/internal/skipped-session.d.ts +8 -0
  50. package/dist/react/use-view.d.ts +3 -3
  51. package/dist/utils.d.ts +22 -54
  52. package/dist/vercel/ably-ai-transport-vercel.js +2297 -2026
  53. package/dist/vercel/ably-ai-transport-vercel.js.map +1 -1
  54. package/dist/vercel/ably-ai-transport-vercel.umd.cjs +1 -1
  55. package/dist/vercel/ably-ai-transport-vercel.umd.cjs.map +1 -1
  56. package/dist/vercel/codec/decode-lifecycle.d.ts +9 -0
  57. package/dist/vercel/codec/events.d.ts +1 -2
  58. package/dist/vercel/codec/fields.d.ts +44 -0
  59. package/dist/vercel/codec/fold-content.d.ts +16 -0
  60. package/dist/vercel/codec/fold-data.d.ts +16 -0
  61. package/dist/vercel/codec/fold-input.d.ts +67 -0
  62. package/dist/vercel/codec/fold-lifecycle.d.ts +16 -0
  63. package/dist/vercel/codec/fold-text.d.ts +16 -0
  64. package/dist/vercel/codec/fold-tool-input.d.ts +17 -0
  65. package/dist/vercel/codec/fold-tool-output.d.ts +16 -0
  66. package/dist/vercel/codec/index.d.ts +5 -30
  67. package/dist/vercel/codec/inputs.d.ts +11 -0
  68. package/dist/vercel/codec/outputs.d.ts +11 -0
  69. package/dist/vercel/codec/reducer-state.d.ts +121 -0
  70. package/dist/vercel/codec/reducer.d.ts +20 -102
  71. package/dist/vercel/codec/tool-transitions.d.ts +0 -6
  72. package/dist/vercel/codec/wire-data.d.ts +34 -0
  73. package/dist/vercel/index.d.ts +1 -0
  74. package/dist/vercel/react/ably-ai-transport-vercel-react.js +2013 -9500
  75. package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
  76. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +1 -70
  77. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
  78. package/dist/vercel/react/contexts/chat-transport-context.d.ts +2 -1
  79. package/dist/vercel/run-end-reason.d.ts +66 -11
  80. package/dist/vercel/tool-part.d.ts +21 -0
  81. package/dist/vercel/transport/chat-transport.d.ts +0 -2
  82. package/dist/vercel/transport/index.d.ts +1 -1
  83. package/dist/vercel/transport/run-output-stream.d.ts +6 -8
  84. package/dist/version.d.ts +1 -1
  85. package/package.json +2 -2
  86. package/src/constants.ts +2 -2
  87. package/src/core/agent.ts +43 -19
  88. package/src/core/channel-options.ts +89 -0
  89. package/src/core/codec/codec-event.ts +27 -0
  90. package/src/core/codec/decoder.ts +145 -21
  91. package/src/core/codec/define-codec.ts +432 -0
  92. package/src/core/codec/encoder.ts +13 -54
  93. package/src/core/codec/field-bag.ts +142 -0
  94. package/src/core/codec/fields.ts +193 -0
  95. package/src/core/codec/index.ts +43 -0
  96. package/src/core/codec/input-descriptor-decoder.ts +97 -0
  97. package/src/core/codec/input-descriptor-encoder.ts +150 -0
  98. package/src/core/codec/input-descriptors.ts +373 -0
  99. package/src/core/codec/output-descriptor-decoder.ts +139 -0
  100. package/src/core/codec/output-descriptor-encoder.ts +101 -0
  101. package/src/core/codec/output-descriptors.ts +307 -0
  102. package/src/core/codec/types.ts +99 -36
  103. package/src/core/codec/well-known-inputs.ts +96 -0
  104. package/src/core/transport/agent-session.ts +330 -589
  105. package/src/core/transport/agent-view.ts +738 -0
  106. package/src/core/transport/client-session.ts +74 -69
  107. package/src/core/transport/decode-fold.ts +57 -47
  108. package/src/core/transport/headers.ts +57 -4
  109. package/src/core/transport/index.ts +2 -1
  110. package/src/core/transport/invocation.ts +1 -1
  111. package/src/core/transport/load-history-pages.ts +220 -0
  112. package/src/core/transport/load-history.ts +63 -61
  113. package/src/core/transport/pipe-stream.ts +10 -1
  114. package/src/core/transport/run-manager.ts +25 -31
  115. package/src/core/transport/session-support.ts +96 -0
  116. package/src/core/transport/tree.ts +414 -47
  117. package/src/core/transport/types/agent.ts +129 -102
  118. package/src/core/transport/types/client.ts +49 -13
  119. package/src/core/transport/types/tree.ts +61 -12
  120. package/src/core/transport/types/view.ts +57 -28
  121. package/src/core/transport/view.ts +520 -172
  122. package/src/core/transport/wire-log.ts +189 -0
  123. package/src/errors.ts +10 -3
  124. package/src/index.ts +44 -11
  125. package/src/react/contexts/client-session-context.ts +1 -1
  126. package/src/react/contexts/client-session-provider.tsx +38 -2
  127. package/src/react/index.ts +2 -1
  128. package/src/react/internal/skipped-session.ts +62 -0
  129. package/src/react/use-client-session.ts +7 -30
  130. package/src/react/use-view.ts +3 -3
  131. package/src/utils.ts +31 -97
  132. package/src/vercel/codec/decode-lifecycle.ts +70 -0
  133. package/src/vercel/codec/events.ts +1 -3
  134. package/src/vercel/codec/fields.ts +58 -0
  135. package/src/vercel/codec/fold-content.ts +54 -0
  136. package/src/vercel/codec/fold-data.ts +46 -0
  137. package/src/vercel/codec/fold-input.ts +255 -0
  138. package/src/vercel/codec/fold-lifecycle.ts +85 -0
  139. package/src/vercel/codec/fold-text.ts +55 -0
  140. package/src/vercel/codec/fold-tool-input.ts +86 -0
  141. package/src/vercel/codec/fold-tool-output.ts +79 -0
  142. package/src/vercel/codec/index.ts +23 -63
  143. package/src/vercel/codec/inputs.ts +116 -0
  144. package/src/vercel/codec/outputs.ts +207 -0
  145. package/src/vercel/codec/reducer-state.ts +169 -0
  146. package/src/vercel/codec/reducer.ts +52 -838
  147. package/src/vercel/codec/tool-transitions.ts +1 -12
  148. package/src/vercel/codec/wire-data.ts +64 -0
  149. package/src/vercel/index.ts +1 -0
  150. package/src/vercel/react/contexts/chat-transport-context.ts +1 -1
  151. package/src/vercel/react/use-chat-transport.ts +8 -28
  152. package/src/vercel/react/use-message-sync.ts +5 -10
  153. package/src/vercel/run-end-reason.ts +95 -16
  154. package/src/vercel/tool-part.ts +25 -0
  155. package/src/vercel/transport/chat-transport.ts +10 -22
  156. package/src/vercel/transport/index.ts +1 -1
  157. package/src/vercel/transport/run-output-stream.ts +7 -8
  158. package/src/version.ts +1 -1
  159. package/dist/core/transport/branch-chain.d.ts +0 -43
  160. package/dist/core/transport/load-conversation.d.ts +0 -128
  161. package/dist/vercel/codec/decoder.d.ts +0 -9
  162. package/dist/vercel/codec/encoder.d.ts +0 -11
  163. package/src/core/transport/branch-chain.ts +0 -58
  164. package/src/core/transport/load-conversation.ts +0 -355
  165. package/src/vercel/codec/decoder.ts +0 -696
  166. package/src/vercel/codec/encoder.ts +0 -548
@@ -1,128 +0,0 @@
1
- import { Logger } from '../../logger.js';
2
- import { Codec, CodecInputEvent, CodecOutputEvent } from '../codec/types.js';
3
- /**
4
- * Agent-side conversation reconstruction from the channel wire log.
5
- *
6
- * When an agent wakes (or resumes) it has no in-memory tree — it rebuilds the
7
- * state it needs by paging channel history and folding the wires through the
8
- * codec. Two entry points:
9
- *
10
- * - {@link loadRunProjection} — fold a single run's wires into one projection
11
- * (used to resume a suspended run with its client tool-output amends).
12
- * - {@link loadConversation} — walk the structural parent chain from the
13
- * current run's input node to the root and fold each node, producing the full
14
- * multi-turn message history along the taken branch.
15
- *
16
- * Both reuse {@link foldMessageInto} (the shared per-message fold primitive) and
17
- * {@link buildBranchChain} (the shared parent-chain walk), so the agent's
18
- * reconstruction can't drift from the client/View decode paths.
19
- */
20
- import * as Ably from 'ably';
21
- /**
22
- * Merge messages observed live (e.g. by the input-event lookup) into a set of
23
- * collected history messages, dedup by serial, and sort chronologically.
24
- *
25
- * History messages take priority in deduplication (history serial wins if the
26
- * same message appears in both). Messages without a serial are dropped because
27
- * they cannot be reliably ordered.
28
- * @param collected - Raw messages from channel.history (any order).
29
- * @param live - Messages observed live (e.g. by the input-event lookup); may be undefined.
30
- * @returns Deduplicated, chronologically sorted messages.
31
- */
32
- export declare const withLiveMessages: (collected: readonly Ably.InboundMessage[], live?: readonly Ably.InboundMessage[]) => Ably.InboundMessage[];
33
- /**
34
- * Fold a pre-sorted array of wire messages for a single run into a projection.
35
- *
36
- * Skips lifecycle events (they carry no codec content) and stops before the
37
- * message whose `codec-message-id` equals `truncateAt` (exclusive — that
38
- * message is not folded). Used by both {@link loadRunProjection} (no
39
- * truncation) and {@link loadConversation} (per-ancestor folding).
40
- * @param codec - Codec used to decode and fold events.
41
- * @param sortedMessages - Chronologically ordered wire messages (all runs).
42
- * @param runId - Only messages stamped with this run-id are folded.
43
- * @param truncateAt - Stop before this codec-message-id; omit to fold all messages.
44
- * @returns The projection and the count of messages that were folded.
45
- */
46
- export declare const foldRunMessages: <TInput extends CodecInputEvent, TOutput extends CodecOutputEvent, TProjection, TMessage>(codec: Codec<TInput, TOutput, TProjection, TMessage>, sortedMessages: readonly Ably.InboundMessage[], runId: string, truncateAt?: string) => {
47
- projection: TProjection;
48
- folded: number;
49
- };
50
- /**
51
- * Fold a single run-less INPUT node's events into a fresh projection: every
52
- * wire stamped with `codecMessageId` and NO run-id (the user prompt the client
53
- * published before the agent minted a run-id). The two-node analogue of
54
- * {@link foldRunMessages} for the user-input side of the conversation chain.
55
- * @param codec - Codec used to decode and fold events.
56
- * @param sortedMessages - Chronologically ordered wire messages (all runs).
57
- * @param codecMessageId - The input node's codec-message-id.
58
- * @returns The folded projection for that input node.
59
- */
60
- export declare const foldInputMessages: <TInput extends CodecInputEvent, TOutput extends CodecOutputEvent, TProjection, TMessage>(codec: Codec<TInput, TOutput, TProjection, TMessage>, sortedMessages: readonly Ably.InboundMessage[], codecMessageId: string) => TProjection;
61
- /**
62
- * Fetch all messages on the channel that belong to `runId`, decode them
63
- * through the codec, and fold them into a single projection. Used by the agent
64
- * to reconstruct a run's full state — including client-published tool-output
65
- * amends — when resuming a suspended run in a fresh agent session.
66
- *
67
- * Doesn't require channel rewind: an explicit `channel.history()` call returns
68
- * the same data even if the channel is already attached from a prior session.
69
- * @param opts - Load parameters.
70
- * @param opts.channel - The Ably channel to read history from.
71
- * @param opts.codec - Codec used to decode and fold events.
72
- * @param opts.runId - Run identifier whose events should be folded.
73
- * @param opts.signal - AbortSignal checked once at entry: if already aborted the call throws immediately and no history is fetched. It does not interrupt an in-flight load.
74
- * @param opts.logger - Optional logger for diagnostic output.
75
- * @param opts.liveMessages - Raw Ably messages already observed live (e.g. by
76
- * the input-event lookup). Folded alongside the history fetch so just-published
77
- * client wires don't depend on Ably's history-indexing window.
78
- * @returns The projection produced by folding all run events in serial order.
79
- * @throws {Ably.ErrorInfo} with code ErrorCode.InvalidArgument if `signal` is already aborted at entry (the run was cancelled before loading began).
80
- */
81
- export declare const loadRunProjection: <TInput extends CodecInputEvent, TOutput extends CodecOutputEvent, TProjection, TMessage>(opts: {
82
- channel: Ably.RealtimeChannel;
83
- codec: Codec<TInput, TOutput, TProjection, TMessage>;
84
- runId: string;
85
- signal: AbortSignal;
86
- logger: Logger | undefined;
87
- liveMessages?: readonly Ably.InboundMessage[];
88
- }) => Promise<TProjection>;
89
- /**
90
- * Reconstruct the full multi-turn conversation history along the branch the
91
- * current run sits on.
92
- *
93
- * Pages channel history (merging live lookup messages), indexes each
94
- * codec-message-id's structural parent and run-id with sticky identity (the
95
- * first wire wins; later amends can't poison it), backfills a reply run's
96
- * parent from its `ai-run-start` when the output wire wasn't indexed, then
97
- * walks the structural parent chain from the current run's input node
98
- * (`assistantParentFallback`) to the root and folds each node in chain order.
99
- * The current run is folded once, wholesale, at the tail.
100
- * @param opts - Reconstruction parameters.
101
- * @param opts.channel - The Ably channel to read history from.
102
- * @param opts.codec - Codec used to decode and fold events.
103
- * @param opts.runId - The current run's id.
104
- * @param opts.signal - AbortSignal checked once at entry; if already aborted the call throws before any history is fetched. It does not interrupt an in-flight load.
105
- * @param opts.logger - Optional logger for diagnostic output.
106
- * @param opts.liveMessages - Wires already observed live, merged into history.
107
- * @param opts.assistantParentFallback - The current run's input node
108
- * (codec-message-id) — the anchor the parent-chain walk starts from. When
109
- * undefined, only the current run is folded.
110
- * @param opts.pageLimit - Messages requested per history page.
111
- * @param opts.maxMessages - Stop paging once this many messages are collected.
112
- * @returns The branch's messages (root-first) and the current run's projection.
113
- * @throws {Ably.ErrorInfo} with code ErrorCode.InvalidArgument when `signal` is already aborted at entry.
114
- */
115
- export declare const loadConversation: <TInput extends CodecInputEvent, TOutput extends CodecOutputEvent, TProjection, TMessage>(opts: {
116
- channel: Ably.RealtimeChannel;
117
- codec: Codec<TInput, TOutput, TProjection, TMessage>;
118
- runId: string;
119
- signal: AbortSignal;
120
- logger: Logger | undefined;
121
- liveMessages: readonly Ably.InboundMessage[] | undefined;
122
- assistantParentFallback: string | undefined;
123
- pageLimit: number;
124
- maxMessages: number;
125
- }) => Promise<{
126
- messages: TMessage[];
127
- projection: TProjection;
128
- }>;
@@ -1,9 +0,0 @@
1
- import { DecoderCoreOptions } from '../../core/codec/decoder.js';
2
- import { Decoder } from '../../core/codec/types.js';
3
- import { VercelInput, VercelOutput } from './events.js';
4
- /**
5
- * Create a Vercel AI SDK decoder that maps Ably messages to {@link DecodedMessage}.
6
- * @param options - Decoder configuration (callbacks, logger).
7
- * @returns A {@link Decoder} typed in both directions for the Vercel codec.
8
- */
9
- export declare const createDecoder: (options?: DecoderCoreOptions) => Decoder<VercelInput, VercelOutput>;
@@ -1,11 +0,0 @@
1
- import { EncoderCoreOptions } from '../../core/codec/encoder.js';
2
- import { ChannelWriter, Encoder } from '../../core/codec/types.js';
3
- import { VercelInput, VercelOutput } from './events.js';
4
- /**
5
- * Create a Vercel AI SDK encoder that maps VercelInput / VercelOutput to
6
- * Ably channel operations via the encoder core.
7
- * @param writer - The channel writer to publish messages through.
8
- * @param options - Encoder configuration (clientId, extras, hooks, logger).
9
- * @returns An {@link Encoder} typed in both directions for the Vercel codec.
10
- */
11
- export declare const createEncoder: (writer: ChannelWriter, options?: EncoderCoreOptions) => Encoder<VercelInput, VercelOutput>;
@@ -1,58 +0,0 @@
1
- /**
2
- * buildBranchChain — order a single conversation branch by walking
3
- * codec-message-id parent links upward from an anchor node to the root.
4
- *
5
- * This is the shared ordering spine of the agent's conversation
6
- * reconstruction and of history decode: both need the same root→anchor
7
- * sequence of nodes before folding each node's projection. Keeping the walk
8
- * here — pure, with no codec, no I/O, no logger — lets it be proven in
9
- * isolation and reused by both engines without drift.
10
- *
11
- * Branch selection is implicit: a node reaches only its own ancestors via
12
- * `parentCodecMessageId`, so sibling branches (edits / regenerates that the
13
- * anchor did not descend from) are never visited. There is no separate
14
- * fork/regenerate filtering step — the un-taken sibling is simply unreachable.
15
- */
16
-
17
- /**
18
- * The single field {@link buildBranchChain} reads from a node. Richer node-meta
19
- * shapes (carrying run-id, fork-of, regenerates, …) satisfy this structurally,
20
- * so callers can pass their full index map directly.
21
- */
22
- export interface BranchChainNode {
23
- /**
24
- * Codec-message-id of this node's structural parent — the node it hangs off
25
- * — or `undefined` for a root node. This is the only edge the walk follows.
26
- */
27
- parentCodecMessageId: string | undefined;
28
- }
29
-
30
- /**
31
- * Walk `parentCodecMessageId` links upward from `anchorCodecMessageId` and
32
- * return the branch it sits on, ordered root-first (oldest) to anchor (newest,
33
- * last). The anchor is always the final element.
34
- *
35
- * The walk stops at the root (a node with no parent), at a dangling parent
36
- * (a parent id absent from `nodeMeta` is still included as the chain head,
37
- * then the walk ends), or on revisiting a node (a cycle in malformed data is
38
- * broken best-effort rather than looping forever).
39
- * @param nodeMeta - Lookup from codec-message-id to its node meta. Need not
40
- * contain the anchor or every ancestor; missing entries simply end the walk.
41
- * @param anchorCodecMessageId - The codec-message-id to start the walk from
42
- * (the newest node on the branch; included in the result).
43
- * @returns The branch's codec-message-ids ordered root-first to anchor-last.
44
- */
45
- export const buildBranchChain = (
46
- nodeMeta: ReadonlyMap<string, BranchChainNode>,
47
- anchorCodecMessageId: string,
48
- ): string[] => {
49
- const chain: string[] = [];
50
- const seen = new Set<string>();
51
- let current: string | undefined = anchorCodecMessageId;
52
- while (current !== undefined && !seen.has(current)) {
53
- seen.add(current);
54
- chain.push(current);
55
- current = nodeMeta.get(current)?.parentCodecMessageId;
56
- }
57
- return chain.toReversed();
58
- };
@@ -1,355 +0,0 @@
1
- /**
2
- * Agent-side conversation reconstruction from the channel wire log.
3
- *
4
- * When an agent wakes (or resumes) it has no in-memory tree — it rebuilds the
5
- * state it needs by paging channel history and folding the wires through the
6
- * codec. Two entry points:
7
- *
8
- * - {@link loadRunProjection} — fold a single run's wires into one projection
9
- * (used to resume a suspended run with its client tool-output amends).
10
- * - {@link loadConversation} — walk the structural parent chain from the
11
- * current run's input node to the root and fold each node, producing the full
12
- * multi-turn message history along the taken branch.
13
- *
14
- * Both reuse {@link foldMessageInto} (the shared per-message fold primitive) and
15
- * {@link buildBranchChain} (the shared parent-chain walk), so the agent's
16
- * reconstruction can't drift from the client/View decode paths.
17
- */
18
-
19
- import * as Ably from 'ably';
20
-
21
- import { EVENT_RUN_START, HEADER_CODEC_MESSAGE_ID, HEADER_PARENT, HEADER_RUN_ID } from '../../constants.js';
22
- import { ErrorCode } from '../../errors.js';
23
- import type { Logger } from '../../logger.js';
24
- import { compareBySerial, getTransportHeaders } from '../../utils.js';
25
- import type { Codec, CodecInputEvent, CodecOutputEvent } from '../codec/types.js';
26
- import type { BranchChainNode } from './branch-chain.js';
27
- import { buildBranchChain } from './branch-chain.js';
28
- import { foldMessageInto } from './decode-fold.js';
29
- import { isRunLifecycleName } from './headers.js';
30
-
31
- // ---------------------------------------------------------------------------
32
- // History collection + dedup
33
- // ---------------------------------------------------------------------------
34
-
35
- /**
36
- * Merge messages observed live (e.g. by the input-event lookup) into a set of
37
- * collected history messages, dedup by serial, and sort chronologically.
38
- *
39
- * History messages take priority in deduplication (history serial wins if the
40
- * same message appears in both). Messages without a serial are dropped because
41
- * they cannot be reliably ordered.
42
- * @param collected - Raw messages from channel.history (any order).
43
- * @param live - Messages observed live (e.g. by the input-event lookup); may be undefined.
44
- * @returns Deduplicated, chronologically sorted messages.
45
- */
46
- export const withLiveMessages = (
47
- collected: readonly Ably.InboundMessage[],
48
- live?: readonly Ably.InboundMessage[],
49
- ): Ably.InboundMessage[] => {
50
- const seen = new Set<string>();
51
- const result: Ably.InboundMessage[] = [];
52
- for (const msg of collected) {
53
- if (msg.serial !== undefined && !seen.has(msg.serial)) {
54
- seen.add(msg.serial);
55
- result.push(msg);
56
- }
57
- }
58
- if (live !== undefined) {
59
- for (const msg of live) {
60
- if (msg.serial !== undefined && !seen.has(msg.serial)) {
61
- seen.add(msg.serial);
62
- result.push(msg);
63
- }
64
- }
65
- }
66
- return result.toSorted(compareBySerial);
67
- };
68
-
69
- /**
70
- * Page through a channel's history and collect raw messages, bounded so a
71
- * long-lived channel can't exhaust memory. No `untilAttach` — callers need
72
- * messages published after the channel first attached (e.g. client tool-output
73
- * amends on a suspended run).
74
- * @param channel - The Ably channel to read history from.
75
- * @param pageLimit - Messages requested per history page.
76
- * @param maxMessages - Stop paging once this many messages are collected.
77
- * @returns The collected messages in history order (newest first per Ably).
78
- */
79
- const collectHistory = async (
80
- channel: Ably.RealtimeChannel,
81
- pageLimit: number,
82
- maxMessages: number,
83
- ): Promise<Ably.InboundMessage[]> => {
84
- const collected: Ably.InboundMessage[] = [];
85
- let page = await channel.history({ limit: pageLimit });
86
- collected.push(...page.items);
87
- while (page.hasNext() && collected.length < maxMessages) {
88
- const nextPage: Ably.PaginatedResult<Ably.InboundMessage> | null = await page.next();
89
- if (!nextPage) break;
90
- collected.push(...nextPage.items);
91
- page = nextPage;
92
- }
93
- return collected;
94
- };
95
-
96
- // ---------------------------------------------------------------------------
97
- // Per-node folds
98
- // ---------------------------------------------------------------------------
99
-
100
- /**
101
- * Fold a pre-sorted array of wire messages for a single run into a projection.
102
- *
103
- * Skips lifecycle events (they carry no codec content) and stops before the
104
- * message whose `codec-message-id` equals `truncateAt` (exclusive — that
105
- * message is not folded). Used by both {@link loadRunProjection} (no
106
- * truncation) and {@link loadConversation} (per-ancestor folding).
107
- * @param codec - Codec used to decode and fold events.
108
- * @param sortedMessages - Chronologically ordered wire messages (all runs).
109
- * @param runId - Only messages stamped with this run-id are folded.
110
- * @param truncateAt - Stop before this codec-message-id; omit to fold all messages.
111
- * @returns The projection and the count of messages that were folded.
112
- */
113
- export const foldRunMessages = <
114
- TInput extends CodecInputEvent,
115
- TOutput extends CodecOutputEvent,
116
- TProjection,
117
- TMessage,
118
- >(
119
- codec: Codec<TInput, TOutput, TProjection, TMessage>,
120
- sortedMessages: readonly Ably.InboundMessage[],
121
- runId: string,
122
- truncateAt?: string,
123
- ): { projection: TProjection; folded: number } => {
124
- const decoder = codec.createDecoder();
125
- let projection = codec.init();
126
- let folded = 0;
127
- for (const msg of sortedMessages) {
128
- const h = getTransportHeaders(msg);
129
- if (h[HEADER_RUN_ID] !== runId) continue;
130
- if (isRunLifecycleName(msg.name)) continue;
131
- const codecMsgId = h[HEADER_CODEC_MESSAGE_ID];
132
- if (truncateAt !== undefined && codecMsgId === truncateAt) break;
133
- projection = foldMessageInto(codec, decoder, projection, msg, codecMsgId ?? '');
134
- folded++;
135
- }
136
- return { projection, folded };
137
- };
138
-
139
- /**
140
- * Fold a single run-less INPUT node's events into a fresh projection: every
141
- * wire stamped with `codecMessageId` and NO run-id (the user prompt the client
142
- * published before the agent minted a run-id). The two-node analogue of
143
- * {@link foldRunMessages} for the user-input side of the conversation chain.
144
- * @param codec - Codec used to decode and fold events.
145
- * @param sortedMessages - Chronologically ordered wire messages (all runs).
146
- * @param codecMessageId - The input node's codec-message-id.
147
- * @returns The folded projection for that input node.
148
- */
149
- export const foldInputMessages = <
150
- TInput extends CodecInputEvent,
151
- TOutput extends CodecOutputEvent,
152
- TProjection,
153
- TMessage,
154
- >(
155
- codec: Codec<TInput, TOutput, TProjection, TMessage>,
156
- sortedMessages: readonly Ably.InboundMessage[],
157
- codecMessageId: string,
158
- ): TProjection => {
159
- const decoder = codec.createDecoder();
160
- let projection = codec.init();
161
- for (const msg of sortedMessages) {
162
- const h = getTransportHeaders(msg);
163
- if (h[HEADER_RUN_ID] !== undefined) continue;
164
- if (h[HEADER_CODEC_MESSAGE_ID] !== codecMessageId) continue;
165
- projection = foldMessageInto(codec, decoder, projection, msg, codecMessageId);
166
- }
167
- return projection;
168
- };
169
-
170
- // ---------------------------------------------------------------------------
171
- // Run-state reconstruction
172
- // ---------------------------------------------------------------------------
173
-
174
- /**
175
- * Fetch all messages on the channel that belong to `runId`, decode them
176
- * through the codec, and fold them into a single projection. Used by the agent
177
- * to reconstruct a run's full state — including client-published tool-output
178
- * amends — when resuming a suspended run in a fresh agent session.
179
- *
180
- * Doesn't require channel rewind: an explicit `channel.history()` call returns
181
- * the same data even if the channel is already attached from a prior session.
182
- * @param opts - Load parameters.
183
- * @param opts.channel - The Ably channel to read history from.
184
- * @param opts.codec - Codec used to decode and fold events.
185
- * @param opts.runId - Run identifier whose events should be folded.
186
- * @param opts.signal - AbortSignal checked once at entry: if already aborted the call throws immediately and no history is fetched. It does not interrupt an in-flight load.
187
- * @param opts.logger - Optional logger for diagnostic output.
188
- * @param opts.liveMessages - Raw Ably messages already observed live (e.g. by
189
- * the input-event lookup). Folded alongside the history fetch so just-published
190
- * client wires don't depend on Ably's history-indexing window.
191
- * @returns The projection produced by folding all run events in serial order.
192
- * @throws {Ably.ErrorInfo} with code ErrorCode.InvalidArgument if `signal` is already aborted at entry (the run was cancelled before loading began).
193
- */
194
- export const loadRunProjection = async <
195
- TInput extends CodecInputEvent,
196
- TOutput extends CodecOutputEvent,
197
- TProjection,
198
- TMessage,
199
- >(opts: {
200
- channel: Ably.RealtimeChannel;
201
- codec: Codec<TInput, TOutput, TProjection, TMessage>;
202
- runId: string;
203
- signal: AbortSignal;
204
- logger: Logger | undefined;
205
- liveMessages?: readonly Ably.InboundMessage[];
206
- }): Promise<TProjection> => {
207
- const { channel, codec, runId, signal, logger, liveMessages } = opts;
208
-
209
- if (signal.aborted) {
210
- throw new Ably.ErrorInfo(
211
- `unable to load run projection; run ${runId} was cancelled`,
212
- ErrorCode.InvalidArgument,
213
- 400,
214
- );
215
- }
216
-
217
- await channel.attach();
218
-
219
- // 2000 wire messages is generously more than any single run could produce.
220
- const collected = await collectHistory(channel, 200, 2000);
221
-
222
- const sorted = withLiveMessages(collected, liveMessages);
223
- const { projection, folded } = foldRunMessages(codec, sorted, runId);
224
-
225
- logger?.debug('loadRunProjection(); folded run events', { runId, folded });
226
- return projection;
227
- };
228
-
229
- /** A node in the reconstruction index — {@link BranchChainNode} plus its run-id. */
230
- interface NodeMeta extends BranchChainNode {
231
- /** The run-id this node belongs to, or undefined for a run-less input node. */
232
- runId: string | undefined;
233
- }
234
-
235
- /**
236
- * Reconstruct the full multi-turn conversation history along the branch the
237
- * current run sits on.
238
- *
239
- * Pages channel history (merging live lookup messages), indexes each
240
- * codec-message-id's structural parent and run-id with sticky identity (the
241
- * first wire wins; later amends can't poison it), backfills a reply run's
242
- * parent from its `ai-run-start` when the output wire wasn't indexed, then
243
- * walks the structural parent chain from the current run's input node
244
- * (`assistantParentFallback`) to the root and folds each node in chain order.
245
- * The current run is folded once, wholesale, at the tail.
246
- * @param opts - Reconstruction parameters.
247
- * @param opts.channel - The Ably channel to read history from.
248
- * @param opts.codec - Codec used to decode and fold events.
249
- * @param opts.runId - The current run's id.
250
- * @param opts.signal - AbortSignal checked once at entry; if already aborted the call throws before any history is fetched. It does not interrupt an in-flight load.
251
- * @param opts.logger - Optional logger for diagnostic output.
252
- * @param opts.liveMessages - Wires already observed live, merged into history.
253
- * @param opts.assistantParentFallback - The current run's input node
254
- * (codec-message-id) — the anchor the parent-chain walk starts from. When
255
- * undefined, only the current run is folded.
256
- * @param opts.pageLimit - Messages requested per history page.
257
- * @param opts.maxMessages - Stop paging once this many messages are collected.
258
- * @returns The branch's messages (root-first) and the current run's projection.
259
- * @throws {Ably.ErrorInfo} with code ErrorCode.InvalidArgument when `signal` is already aborted at entry.
260
- */
261
- export const loadConversation = async <
262
- TInput extends CodecInputEvent,
263
- TOutput extends CodecOutputEvent,
264
- TProjection,
265
- TMessage,
266
- >(opts: {
267
- channel: Ably.RealtimeChannel;
268
- codec: Codec<TInput, TOutput, TProjection, TMessage>;
269
- runId: string;
270
- signal: AbortSignal;
271
- logger: Logger | undefined;
272
- liveMessages: readonly Ably.InboundMessage[] | undefined;
273
- assistantParentFallback: string | undefined;
274
- pageLimit: number;
275
- maxMessages: number;
276
- }): Promise<{ messages: TMessage[]; projection: TProjection }> => {
277
- const { channel, codec, runId, signal, logger, liveMessages, assistantParentFallback, pageLimit, maxMessages } = opts;
278
-
279
- if (signal.aborted) {
280
- throw new Ably.ErrorInfo(`unable to load conversation; run ${runId} was cancelled`, ErrorCode.InvalidArgument, 400);
281
- }
282
-
283
- // Single channel.history() fetch for all runs. Live lookup messages are
284
- // merged in so the current run's just-published client wires don't depend on
285
- // Ably's history-indexing window. Deduped by serial (history wins), sorted.
286
- const collected = await collectHistory(channel, pageLimit, maxMessages);
287
- const sortedMessages = withLiveMessages(collected, liveMessages);
288
-
289
- // Index pass — node metadata per codec-message-id from the serial-sorted
290
- // history, with sticky identity (the first wire for a codec-message-id wins
291
- // for the structural parent; a later amend can't poison it). Run-bearing
292
- // wires record their runId; run-less user inputs are input nodes (runId
293
- // undefined).
294
- const nodeMeta = new Map<string, NodeMeta>();
295
- const runIdToCodecMessageId = new Map<string, string>();
296
- for (const msg of sortedMessages) {
297
- if (isRunLifecycleName(msg.name)) continue;
298
- const h = getTransportHeaders(msg);
299
- const cid = h[HEADER_CODEC_MESSAGE_ID];
300
- if (cid === undefined) continue;
301
- const msgRunId = h[HEADER_RUN_ID];
302
- if (msgRunId !== undefined) runIdToCodecMessageId.set(msgRunId, cid);
303
- if (!nodeMeta.has(cid)) {
304
- nodeMeta.set(cid, { runId: msgRunId, parentCodecMessageId: h[HEADER_PARENT] });
305
- }
306
- }
307
- // Backfill a reply run's structural parent from ai-run-start when its output
308
- // wire wasn't indexed (rare history lag). Keyed by runId → codec-message-id.
309
- for (const msg of sortedMessages) {
310
- if (msg.name !== EVENT_RUN_START) continue;
311
- const h = getTransportHeaders(msg);
312
- const msgRunId = h[HEADER_RUN_ID];
313
- if (msgRunId === undefined) continue;
314
- const cid = runIdToCodecMessageId.get(msgRunId);
315
- if (cid === undefined) continue;
316
- const meta = nodeMeta.get(cid);
317
- if (meta && meta.parentCodecMessageId === undefined) meta.parentCodecMessageId = h[HEADER_PARENT];
318
- }
319
-
320
- // Walk the structural parent chain from the current run's input node up to
321
- // the conversation root, then fold each node in chain order. The upward walk
322
- // naturally excludes un-taken branch siblings (an edit's alternate prompt, a
323
- // regenerate's superseded reply), so no per-ancestor truncation is needed.
324
- // (Open caveat, deferred with a golden test: regenerating a non-trailing
325
- // message of a multi-message reply — the node walk can't slice inside one
326
- // run's projection.)
327
- const messages: TMessage[] = [];
328
- let chainLength = 0;
329
- if (assistantParentFallback !== undefined) {
330
- const chain = buildBranchChain(nodeMeta, assistantParentFallback);
331
- chainLength = chain.length;
332
- for (const cid of chain) {
333
- const meta = nodeMeta.get(cid);
334
- // Skip any chain node belonging to the CURRENT run — it is folded once,
335
- // wholesale, at the tail below. For a continuation the run-id is reused
336
- // and `assistantParentFallback` points at a message INSIDE the current
337
- // run, so it would otherwise be folded twice and emit duplicate tool_use
338
- // ids.
339
- if (meta?.runId === runId) continue;
340
- const projection =
341
- meta?.runId === undefined
342
- ? foldInputMessages(codec, sortedMessages, cid)
343
- : foldRunMessages(codec, sortedMessages, meta.runId).projection;
344
- messages.push(...codec.getMessages(projection).map((m) => m.message));
345
- }
346
- }
347
-
348
- // Current run — folded from the same sorted messages, appended at the chain
349
- // tail (the chain ended at this run's input node).
350
- const { projection, folded } = foldRunMessages(codec, sortedMessages, runId);
351
- messages.push(...codec.getMessages(projection).map((m) => m.message));
352
-
353
- logger?.debug('loadConversation(); built', { runId, chainLength, totalMessages: messages.length, folded });
354
- return { messages, projection };
355
- };