1/**
2 * Provides identifier generation for AI features.
3 *
4 * The `IdGenerator` service exposes one operation, `generateId`, which returns
5 * a string inside `Effect`. AI modules use it for values such as tool call ids
6 * and generated response item ids. This module includes the service tag,
7 * service interface, default generator, configurable custom generator, and layer
8 * for providing the service.
9 *
10 * @stability unstable
11 * @since 4.0.0
12 */
13import * as Cause from "../Cause.ts"
14import * as Context from "../Context.ts"
15import * as Effect from "../Effect.ts"
16import * as Layer from "../Layer.ts"
17import * as Predicate from "../Predicate.ts"
18import * as Random from "../Random.ts"
19
20/**
21 * Service tag for AI identifier generation services.
22 *
23 * **When to use**
24 *
25 * Use to access or provide the service that creates identifiers for AI tool
26 * calls and related generated values.
27 *
28 * **Details**
29 *
30 * This tag is used to provide and access ID generation functionality throughout
31 * the application. It follows Effect's standard service pattern for type-safe
32 * dependency injection.
33 *
34 * **Example** (Accessing the ID generator service)
35 *
36 * ```ts import.meta.vitest
37 * import { Effect } from "effect"
38 * import { IdGenerator } from "effect/ai"
39 *
40 * const useIdGenerator = Effect.gen(function*() {
41 * const idGenerator = yield* IdGenerator.IdGenerator
42 * const newId = yield* idGenerator.generateId()
43 * return newId
44 * })
45 *
46 * const program = useIdGenerator.pipe(
47 * Effect.provideService(IdGenerator.IdGenerator, {
48 * generateId: () => Effect.succeed("id-1")
49 * })
50 * )
51 * await Effect.runPromise(program) // => "id-1"
52 * ```
53 *
54 * @stability unstable
55 * @category services
56 * @since 4.0.0
57 */
58export class IdGenerator extends Context.Service<IdGenerator, Service>()(
59 "@effect/ai/IdGenerator"
60) {}
61
62/**
63 * The service interface for ID generation.
64 *
65 * **Details**
66 *
67 * Defines the contract that all ID generator implementations must fulfill.
68 * The service provides a single method for generating unique identifiers
69 * in an effectful context.
70 *
71 * **Example** (Implementing a custom ID generator)
72 *
73 * ```ts import.meta.vitest
74 * import { Effect } from "effect"
75 * import type { IdGenerator } from "effect/ai"
76 *
77 * // Custom deterministic implementation
78 * let nextId = 0
79 * const customService: IdGenerator.Service = {
80 * generateId: () => Effect.sync(() => `custom_${++nextId}`)
81 * }
82 *
83 * const program = customService.generateId()
84 *
85 * await Effect.runPromise(program) // => "custom_1"
86 * ```
87 *
88 * @stability unstable
89 * @category models
90 * @since 4.0.0
91 */
92export interface Service {
93 readonly generateId: () => Effect.Effect<string>
94}
95
96/**
97 * Configuration options for creating custom ID generators.
98 *
99 * **Example** (Configuring generated IDs)
100 *
101 * ```ts import.meta.vitest
102 * import type { IdGenerator } from "effect/ai"
103 *
104 * // Configuration for tool call IDs
105 * const toolCallOptions: IdGenerator.MakeOptions = {
106 * alphabet: "0123456789ABCDEF",
107 * prefix: "tool",
108 * separator: "_",
109 * size: 8
110 * }
111 *
112 * // This will generate IDs like: "tool_A1B2C3D4"
113 * const result = [toolCallOptions.prefix, toolCallOptions.size] // => ["tool", 8]
114 * ```
115 *
116 * @stability unstable
117 * @category options
118 * @since 4.0.0
119 */
120export interface MakeOptions {
121 /**
122 * The character set to use for generating the random portion of IDs.
123 */
124 readonly alphabet: string
125 /**
126 * Optional prefix to prepend to generated IDs.
127 */
128 readonly prefix?: string | undefined
129 /**
130 * Character used to separate the prefix from the random portion.
131 */
132 readonly separator: string
133 /**
134 * Length of the random portion of the generated ID.
135 */
136 readonly size: number
137}
138
139const DEFAULT_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
140const DEFAULT_SEPARATOR = "_"
141const DEFAULT_SIZE = 16
142
143const makeGenerator = ({
144 alphabet = DEFAULT_ALPHABET,
145 prefix,
146 separator = DEFAULT_SEPARATOR,
147 size = DEFAULT_SIZE
148}: Partial<MakeOptions>) => {
149 const alphabetLength = alphabet.length
150 return Effect.fnUntraced(function*() {
151 const chars = new Array(size)
152 for (let i = 0; i < size; i++) {
153 const index = yield* Random.next
154 chars[i] = alphabet[(index * alphabetLength) | 0]
155 }
156 const identifier = chars.join("")
157 if (Predicate.isUndefined(prefix)) {
158 return identifier
159 }
160 return `${prefix}${separator}${identifier}`
161 })
162}
163
164/**
165 * Default ID generator service implementation.
166 *
167 * **Details**
168 *
169 * Uses the standard configuration with "id" prefix and generates IDs in the
170 * format "id_XXXXXXXXXXXXXXXX" where X represents random alphanumeric
171 * characters.
172 *
173 * **Example** (Generating default IDs)
174 *
175 * ```ts import.meta.vitest
176 * import { Effect } from "effect"
177 * import { IdGenerator } from "effect/ai"
178 *
179 * const program = Effect.gen(function*() {
180 * const id = yield* IdGenerator.defaultIdGenerator.generateId()
181 * return id
182 * })
183 *
184 * // Or provide it as a service
185 * const withDefault = program.pipe(
186 * Effect.provideService(
187 * IdGenerator.IdGenerator,
188 * IdGenerator.defaultIdGenerator
189 * )
190 * )
191 *
192 * const id = await Effect.runPromise(withDefault)
193 * const result = [id.startsWith("id_"), id.length] // => [true, 19]
194 * ```
195 *
196 * @stability unstable
197 * @category constructors
198 * @since 4.0.0
199 */
200export const defaultIdGenerator: Service = {
201 generateId: makeGenerator({ prefix: "id" })
202}
203
204/**
205 * Creates a custom ID generator service with the specified options.
206 *
207 * **Details**
208 *
209 * Validates the configuration to ensure the separator is not part of the
210 * alphabet, which would cause ambiguity in parsing generated IDs.
211 *
212 * **Example** (Creating a custom generator)
213 *
214 * ```ts import.meta.vitest
215 * import { Effect } from "effect"
216 * import { IdGenerator } from "effect/ai"
217 *
218 * const program = Effect.gen(function*() {
219 * // Create a generator for AI assistant message IDs
220 * const messageIdGen = yield* IdGenerator.make({
221 * alphabet: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ",
222 * prefix: "msg",
223 * separator: "-",
224 * size: 10
225 * })
226 *
227 * return yield* messageIdGen.generateId()
228 * })
229 *
230 * const messageId = await Effect.runPromise(program)
231 * const result = [messageId.startsWith("msg-"), messageId.length] // => [true, 14]
232 * ```
233 *
234 * **Example** (Handling invalid generator options)
235 *
236 * ```ts import.meta.vitest
237 * import { Effect } from "effect"
238 * import { IdGenerator } from "effect/ai"
239 *
240 * // This will fail with IllegalArgumentError
241 * const invalidConfig = IdGenerator.make({
242 * alphabet: "ABC123",
243 * prefix: "test",
244 * separator: "A", // Error: separator is part of alphabet
245 * size: 8
246 * })
247 *
248 * const error = await Effect.runPromise(Effect.flip(invalidConfig))
249 * error.message // => 'The separator "A" must not be part of the alphabet "ABC123".'
250 * ```
251 *
252 * @stability unstable
253 * @category constructors
254 * @since 4.0.0
255 */
256export const make = Effect.fnUntraced(function*({
257 alphabet = DEFAULT_ALPHABET,
258 prefix,
259 separator = DEFAULT_SEPARATOR,
260 size = DEFAULT_SIZE
261}: MakeOptions) {
262 if (alphabet.includes(separator)) {
263 const message = `The separator "${separator}" must not be part of the alphabet "${alphabet}".`
264 return yield* new Cause.IllegalArgumentError(message)
265 }
266
267 const generateId = makeGenerator({ alphabet, prefix, separator, size })
268
269 return {
270 generateId
271 } as const
272})
273
274/**
275 * Creates a Layer that provides the IdGenerator service with custom
276 * configuration.
277 *
278 * **When to use**
279 *
280 * Use when you need to provide ID generation capabilities from validated
281 * configuration.
282 *
283 * **Example** (Providing an ID generator layer)
284 *
285 * ```ts import.meta.vitest
286 * import { Effect } from "effect"
287 * import { IdGenerator } from "effect/ai"
288 *
289 * // Create a layer for generating AI tool call IDs
290 * const toolCallIdLayer = IdGenerator.layer({
291 * alphabet: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ",
292 * prefix: "tool_call",
293 * separator: "_",
294 * size: 12
295 * })
296 *
297 * const program = Effect.gen(function*() {
298 * const idGen = yield* IdGenerator.IdGenerator
299 * return yield* idGen.generateId()
300 * }).pipe(Effect.provide(toolCallIdLayer))
301 *
302 * const toolCallId = await Effect.runPromise(program)
303 * const result = [toolCallId.startsWith("tool_call_"), toolCallId.length] // => [true, 22]
304 * ```
305 *
306 * @stability unstable
307 * @category layers
308 * @since 4.0.0
309 */
310export const layer = (options: MakeOptions): Layer.Layer<IdGenerator, Cause.IllegalArgumentError> =>
311 Layer.effect(IdGenerator)(make(options))