1/**
2 * Track provider response IDs for incremental language model calls.
3 *
4 * Some providers can continue from a prior response by accepting a
5 * `previousResponseId` plus only the messages added after that response. This
6 * module exposes a small mutable service that remembers which prompt message
7 * objects were included in each provider response and prepares a shorter prompt
8 * when a later call extends the same conversation.
9 *
10 * @stability unstable
11 * @since 4.0.0
12 */
13import * as Context from "../Context.ts"
14import * as Effect from "../Effect.ts"
15import * as Option from "../Option.ts"
16import * as Prompt from "./Prompt.ts"
17
18/**
19 * Result returned when a tracked prompt can be sent incrementally.
20 *
21 * **Details**
22 *
23 * It contains the provider response ID to pass as `previousResponseId` and the
24 * prompt fragment containing only the new messages after the latest assistant
25 * turn.
26 *
27 * @stability unstable
28 * @category models
29 * @since 4.0.0
30 */
31export interface PrepareResult {
32 readonly previousResponseId: string
33 readonly prompt: Prompt.Prompt
34}
35
36/**
37 * Mutable service that tracks prompt message object identities by provider
38 * response ID.
39 *
40 * **Details**
41 *
42 * `markParts` records the prompt messages that produced a response,
43 * `prepareUnsafe` returns a `previousResponseId` plus the untracked suffix when
44 * the prompt prefix is fully recognized, and `clearUnsafe` drops all tracked
45 * state.
46 *
47 * @stability unstable
48 * @category models
49 * @since 4.0.0
50 */
51export interface Service {
52 clearUnsafe(): void
53 markParts(parts: ReadonlyArray<object>, responseId: string): void
54 prepareUnsafe(prompt: Prompt.Prompt): Option.Option<PrepareResult>
55}
56
57/**
58 * Service tag for enabling provider previous-response ID reuse across language
59 * model calls.
60 *
61 * **When to use**
62 *
63 * Use when you provide a language model with previous-response ID tracking so
64 * later calls can send only new prompt messages together with the provider's
65 * prior response ID.
66 *
67 * @stability unstable
68 * @category services
69 * @since 4.0.0
70 */
71export class ResponseIdTracker extends Context.Service<ResponseIdTracker, Service>()("effect/ai/ResponseIdTracker") {}
72
73/**
74 * Creates an in-memory `ResponseIdTracker` service.
75 *
76 * **Details**
77 *
78 * The tracker maps prompt message object identities to provider response IDs.
79 * `prepareUnsafe` returns a previous response ID and the messages after the
80 * latest assistant turn only when the existing prompt prefix is fully tracked;
81 * otherwise it clears the tracked state and returns `Option.none()`.
82 *
83 * @stability unstable
84 * @category constructors
85 * @since 4.0.0
86 */
87export const make: Effect.Effect<Service> = Effect.sync(() => {
88 const sentParts = new Map<object, string>()
89
90 const none = () => {
91 sentParts.clear()
92 return Option.none<PrepareResult>()
93 }
94
95 return {
96 clearUnsafe() {
97 sentParts.clear()
98 },
99 markParts(parts, responseId) {
100 for (let i = 0; i < parts.length; i++) {
101 sentParts.set(parts[i], responseId)
102 }
103 },
104 prepareUnsafe(prompt) {
105 const messages = prompt.content
106
107 let anyTracked = false
108 for (let i = 0; i < messages.length; i++) {
109 if (sentParts.has(messages[i])) {
110 anyTracked = true
111 break
112 }
113 }
114 if (!anyTracked) return none()
115
116 let lastAssistantIndex = -1
117 for (let i = messages.length - 1; i >= 0; i--) {
118 if (messages[i].role === "assistant") {
119 lastAssistantIndex = i
120 break
121 }
122 }
123 if (lastAssistantIndex === -1) return none()
124
125 let responseId: string | undefined
126 for (let i = 0; i < lastAssistantIndex; i++) {
127 const id = sentParts.get(messages[i])
128 if (id === undefined) return none()
129 responseId = id
130 }
131 if (responseId === undefined) return none()
132
133 const partsAfterLastAssistant = messages.slice(lastAssistantIndex + 1)
134 if (partsAfterLastAssistant.length === 0) {
135 return none()
136 }
137
138 return Option.some({
139 previousResponseId: responseId,
140 prompt: Prompt.fromMessages(partsAfterLastAssistant)
141 })
142 }
143 }
144})