@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
|
@@ -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
|
-
};
|