1import {
2 CheckpointId,
3 CheckpointScopeId,
4 CommandId,
5 ContextHandoffId,
6 ContextTransferId,
7 EventId,
8 MessageId,
9 NodeId,
10 PlanId,
11 ProjectId,
12 ProviderDriverKind,
13 ProviderInstanceId,
14 ProviderSessionId,
15 ProviderThreadId,
16 ProviderTurnId,
17 RawEventId,
18 RunAttemptId,
19 RunId,
20 RuntimeRequestId,
21 ThreadId,
22 TurnItemId,
23} from "@t3tools/contracts";
24// @effect-diagnostics-next-line nodeBuiltinImport:off -- derive ids are synchronous, so the hash must be too.
25import * as NodeCrypto from "node:crypto";
26
27import * as Context from "effect/Context";
28import * as Effect from "effect/Effect";
29import * as Layer from "effect/Layer";
30import * as Schema from "effect/Schema";
31
32import { randomUuidV4 } from "./randomUuid.ts";
33
34export const IdAllocatorV2Kind = Schema.Literals([
35 "command",
36 "event",
37 "raw_event",
38 "project",
39 "thread",
40 "message",
41 "run",
42 "run_attempt",
43 "node",
44 "provider_session",
45 "provider_thread",
46 "provider_turn",
47 "runtime_request",
48 "turn_item",
49 "checkpoint_scope",
50 "checkpoint",
51 "context_handoff",
52 "context_transfer",
53 "plan",
54]);
55export type IdAllocatorV2Kind = typeof IdAllocatorV2Kind.Type;
56
57export class IdAllocatorV2AllocationError extends Schema.TaggedError<IdAllocatorV2AllocationError>()(
58 "IdAllocatorV2AllocationError",
59 {
60 kind: IdAllocatorV2Kind,
61 input: Schema.optional(Schema.Unknown),
62 cause: Schema.optional(Schema.Defect()),
63 },
64) {
65 override get message(): string {
66 return `Failed to allocate orchestration ${this.kind} id.`;
67 }
68}
69
70export const IdAllocatorV2Error = Schema.Union([IdAllocatorV2AllocationError]);
71export type IdAllocatorV2Error = typeof IdAllocatorV2Error.Type;
72
73export interface IdAllocatorV2Allocate {
74 readonly command: (input: {
75 readonly fixtureName: string;
76 readonly commandName: string;
77 }) => Effect.Effect<CommandId, IdAllocatorV2Error>;
78 readonly event: (input: {
79 readonly threadId?: ThreadId;
80 readonly commandId?: CommandId;
81 readonly providerSessionId?: ProviderSessionId;
82 }) => Effect.Effect<EventId, IdAllocatorV2Error>;
83 readonly rawEvent: (input: {
84 readonly providerSessionId: ProviderSessionId;
85 readonly method: string | null;
86 }) => Effect.Effect<RawEventId, IdAllocatorV2Error>;
87 readonly project: (input: {
88 readonly fixtureName: string;
89 }) => Effect.Effect<ProjectId, IdAllocatorV2Error>;
90 readonly thread: (input: {
91 readonly fixtureName?: string;
92 readonly projectId?: ProjectId;
93 }) => Effect.Effect<ThreadId, IdAllocatorV2Error>;
94 readonly message: (input: {
95 readonly threadId: ThreadId;
96 readonly ordinal: number;
97 }) => Effect.Effect<MessageId, IdAllocatorV2Error>;
98 readonly providerSession: (input: {
99 readonly providerInstanceId: ProviderInstanceId;
100 readonly threadId: ThreadId;
101 }) => Effect.Effect<ProviderSessionId, IdAllocatorV2Error>;
102 readonly runtimeRequest: (input: {
103 readonly driver: ProviderDriverKind;
104 readonly providerTurnId?: ProviderTurnId;
105 readonly nativeRequestId?: string;
106 }) => Effect.Effect<RuntimeRequestId, IdAllocatorV2Error>;
107 readonly checkpointScope: (input: {
108 readonly threadId: ThreadId;
109 readonly name: string;
110 }) => Effect.Effect<CheckpointScopeId, IdAllocatorV2Error>;
111 readonly checkpoint: (input: {
112 readonly checkpointScopeId: CheckpointScopeId;
113 readonly name: string;
114 }) => Effect.Effect<CheckpointId, IdAllocatorV2Error>;
115 readonly contextHandoff: (input: {
116 readonly threadId: ThreadId;
117 readonly fromProviderInstanceId: ProviderInstanceId;
118 readonly toProviderInstanceId: ProviderInstanceId;
119 }) => Effect.Effect<ContextHandoffId, IdAllocatorV2Error>;
120 readonly contextTransfer: (input: {
121 readonly sourceThreadId: ThreadId;
122 readonly targetThreadId: ThreadId;
123 readonly type: string;
124 }) => Effect.Effect<ContextTransferId, IdAllocatorV2Error>;
125 readonly plan: (input: {
126 readonly threadId: ThreadId;
127 readonly runId?: RunId;
128 readonly driver: ProviderDriverKind;
129 }) => Effect.Effect<PlanId, IdAllocatorV2Error>;
130}
131
132export interface IdAllocatorV2Derive {
133 readonly providerSession: (input: {
134 readonly providerInstanceId: ProviderInstanceId;
135 }) => ProviderSessionId;
136 readonly delegatedTaskNode: (input: { readonly commandId: CommandId }) => NodeId;
137 readonly delegatedTaskThread: (input: { readonly commandId: CommandId }) => ThreadId;
138 readonly delegatedTaskMessage: (input: { readonly commandId: CommandId }) => MessageId;
139 readonly delegatedTaskTurnItem: (input: { readonly commandId: CommandId }) => TurnItemId;
140 readonly createdThreadTurnItem: (input: { readonly commandId: CommandId }) => TurnItemId;
141 readonly threadFromProviderThread: (input: {
142 readonly driver: ProviderDriverKind;
143 readonly nativeThreadId: string;
144 readonly providerInstanceId?: ProviderInstanceId;
145 }) => ThreadId;
146 readonly subagentThread: (input: {
147 readonly driver: ProviderDriverKind;
148 readonly nativeThreadId: string;
149 readonly providerInstanceId?: ProviderInstanceId;
150 }) => ThreadId;
151 readonly run: (input: { readonly threadId: ThreadId; readonly ordinal: number }) => RunId;
152 readonly runAttempt: (input: {
153 readonly runId: RunId;
154 readonly attemptOrdinal: number;
155 }) => RunAttemptId;
156 readonly rootNode: (input: { readonly runId: RunId }) => NodeId;
157 readonly rootNodeAttempt: (input: {
158 readonly runId: RunId;
159 readonly attemptOrdinal: number;
160 }) => NodeId;
161 readonly userTurnItem: (input: { readonly messageId: MessageId }) => TurnItemId;
162 readonly runSignalTurnItem: (input: {
163 readonly runId: RunId;
164 readonly signal: string;
165 }) => TurnItemId;
166 readonly providerThread: (input: {
167 readonly driver: ProviderDriverKind;
168 readonly nativeThreadId: string;
169 readonly providerInstanceId?: ProviderInstanceId;
170 }) => ProviderThreadId;
171 readonly providerTurn: (input: {
172 readonly driver: ProviderDriverKind;
173 readonly nativeTurnId: string;
174 }) => ProviderTurnId;
175 readonly nodeFromProviderItem: (input: {
176 readonly driver: ProviderDriverKind;
177 readonly nativeItemId: string;
178 }) => NodeId;
179 readonly messageFromProviderItem: (input: {
180 readonly driver: ProviderDriverKind;
181 readonly nativeItemId: string;
182 }) => MessageId;
183 readonly turnItemFromProviderItem: (input: {
184 readonly driver: ProviderDriverKind;
185 readonly nativeItemId: string;
186 }) => TurnItemId;
187 readonly approvalNode: (input: { readonly requestId: RuntimeRequestId }) => NodeId;
188 readonly approvalTurnItem: (input: { readonly requestId: RuntimeRequestId }) => TurnItemId;
189}
190
191export class IdAllocatorV2 extends Context.Service<
192 IdAllocatorV2,
193 {
194 readonly allocate: IdAllocatorV2Allocate;
195 readonly derive: IdAllocatorV2Derive;
196 }
197>()("@t3tools/provider-core/server/IdAllocator/IdAllocatorV2") {}
198
199const encodePart = (part: string | number): string => encodeURIComponent(String(part));
200
201const joinId = (prefix: string, ...parts: ReadonlyArray<string | number>): string =>
202 [prefix, ...parts.map(encodePart)].join(":");
203
204export function deriveThreadFromProviderThread(input: {
205 readonly driver: ProviderDriverKind;
206 readonly providerInstanceId?: ProviderInstanceId;
207 readonly nativeThreadId: string;
208}): ThreadId {
209 return ThreadId.make(
210 joinId(
211 "thread",
212 "provider",
213 input.driver,
214 ...(input.providerInstanceId === undefined
215 ? []
216 : ["provider-instance", input.providerInstanceId]),
217 "native-thread",
218 input.nativeThreadId,
219 ),
220 );
221}
222
223// Agents quote subagent thread ids back to thread tools, and the native id often
224// nests the parent's encoded ids, so hash it to a short stable id instead. Adapters
225// re-derive child ids on resume, so a child stored under its legacy id keeps it.
226const makeDeriveSubagentThread =
227 (legacyThreadIds: ReadonlySet<string>): IdAllocatorV2Derive["subagentThread"] =>
228 (input) => {
229 const legacy = deriveThreadFromProviderThread(input);
230 if (legacyThreadIds.has(legacy)) return legacy;
231 const digest = NodeCrypto.createHash("sha256").update(legacy).digest("hex").slice(0, 16);
232 return ThreadId.make(`thread:subagent:${digest}`);
233 };
234
235export function deriveProviderThread(input: {
236 readonly driver: ProviderDriverKind;
237 readonly providerInstanceId?: ProviderInstanceId;
238 readonly nativeThreadId: string;
239}): ProviderThreadId {
240 return ProviderThreadId.make(
241 joinId(
242 "provider-thread",
243 "provider",
244 input.driver,
245 ...(input.providerInstanceId === undefined
246 ? []
247 : ["provider-instance", input.providerInstanceId]),
248 "native-thread",
249 input.nativeThreadId,
250 ),
251 );
252}
253
254const randomId =
255 <Id, Input>(input: {
256 readonly kind: IdAllocatorV2Kind;
257 readonly prefix: string;
258 readonly parts: ReadonlyArray<string | number>;
259 readonly make: (value: string) => Id;
260 }) =>
261 (allocationInput: Input): Effect.Effect<Id, IdAllocatorV2Error> =>
262 randomUuidV4.pipe(
263 Effect.map((uuid) => input.make(joinId(input.prefix, ...input.parts, uuid))),
264 Effect.mapError(
265 (cause) =>
266 new IdAllocatorV2AllocationError({
267 kind: input.kind,
268 input: allocationInput,
269 cause,
270 }),
271 ),
272 );
273
274export const derive: IdAllocatorV2Derive = {
275 providerSession: (input) =>
276 ProviderSessionId.make(
277 joinId("provider-session", "provider-instance", input.providerInstanceId, "shared"),
278 ),
279 delegatedTaskNode: (input) => NodeId.make(joinId("node", "delegated-task", input.commandId)),
280 delegatedTaskThread: (input) =>
281 ThreadId.make(joinId("thread", "delegated-task", input.commandId)),
282 delegatedTaskMessage: (input) =>
283 MessageId.make(joinId("message", "delegated-task", input.commandId)),
284 delegatedTaskTurnItem: (input) =>
285 TurnItemId.make(joinId("turn-item", "delegated-task", input.commandId)),
286 createdThreadTurnItem: (input) =>
287 TurnItemId.make(joinId("turn-item", "created-thread", input.commandId)),
288 threadFromProviderThread: deriveThreadFromProviderThread,
289 subagentThread: makeDeriveSubagentThread(new Set()),
290 run: (input) => RunId.make(joinId("run", "thread", input.threadId, "ordinal", input.ordinal)),
291 runAttempt: (input) =>
292 RunAttemptId.make(joinId("run-attempt", "run", input.runId, "attempt", input.attemptOrdinal)),
293 rootNode: (input) => NodeId.make(joinId("node", "run", input.runId, "root")),
294 rootNodeAttempt: (input) =>
295 NodeId.make(joinId("node", "run", input.runId, "attempt", input.attemptOrdinal, "root")),
296 userTurnItem: (input) => TurnItemId.make(joinId("turn-item", "message", input.messageId)),
297 runSignalTurnItem: (input) =>
298 TurnItemId.make(joinId("turn-item", "run", input.runId, "signal", input.signal)),
299 providerThread: deriveProviderThread,
300 providerTurn: (input) =>
301 ProviderTurnId.make(
302 joinId("provider-turn", "provider", input.driver, "native-turn", input.nativeTurnId),
303 ),
304 nodeFromProviderItem: (input) =>
305 NodeId.make(joinId("node", "provider", input.driver, "native-item", input.nativeItemId)),
306 messageFromProviderItem: (input) =>
307 MessageId.make(joinId("message", "provider", input.driver, "native-item", input.nativeItemId)),
308 turnItemFromProviderItem: (input) =>
309 TurnItemId.make(
310 joinId("turn-item", "provider", input.driver, "native-item", input.nativeItemId),
311 ),
312 approvalNode: (input) => NodeId.make(joinId("node", "runtime-request", input.requestId)),
313 approvalTurnItem: (input) =>
314 TurnItemId.make(joinId("turn-item", "runtime-request", input.requestId)),
315};
316
317export const layer: Layer.Layer<IdAllocatorV2> = Layer.succeed(
318 IdAllocatorV2,
319 IdAllocatorV2.of({
320 allocate: {
321 command: (input) =>
322 randomId<CommandId, typeof input>({
323 kind: "command",
324 prefix: "command",
325 parts: ["fixture", input.fixtureName, input.commandName],
326 make: CommandId.make,
327 })(input),
328 event: (input) =>
329 randomId<EventId, typeof input>({
330 kind: "event",
331 prefix: "event",
332 parts: [
333 ...(input.threadId === undefined ? [] : ["thread", input.threadId]),
334 ...(input.commandId === undefined ? [] : ["command", input.commandId]),
335 ...(input.providerSessionId === undefined
336 ? []
337 : ["provider-session", input.providerSessionId]),
338 ],
339 make: EventId.make,
340 })(input),
341 rawEvent: (input) =>
342 randomId<RawEventId, typeof input>({
343 kind: "raw_event",
344 prefix: "raw-event",
345 parts: [
346 "provider-session",
347 input.providerSessionId,
348 ...(input.method === null ? [] : ["method", input.method]),
349 ],
350 make: RawEventId.make,
351 })(input),
352 project: (input) =>
353 randomId<ProjectId, typeof input>({
354 kind: "project",
355 prefix: "project",
356 parts: ["fixture", input.fixtureName],
357 make: ProjectId.make,
358 })(input),
359 thread: (input) =>
360 randomId<ThreadId, typeof input>({
361 kind: "thread",
362 prefix: "thread",
363 parts: [
364 ...(input.fixtureName === undefined ? [] : ["fixture", input.fixtureName]),
365 ...(input.projectId === undefined ? [] : ["project", input.projectId]),
366 ],
367 make: ThreadId.make,
368 })(input),
369 message: (input) =>
370 randomId<MessageId, typeof input>({
371 kind: "message",
372 prefix: "message",
373 parts: ["thread", input.threadId, "ordinal", input.ordinal],
374 make: MessageId.make,
375 })(input),
376 providerSession: (input) =>
377 randomId<ProviderSessionId, typeof input>({
378 kind: "provider_session",
379 prefix: "provider-session",
380 parts: ["provider-instance", input.providerInstanceId, "thread", input.threadId],
381 make: ProviderSessionId.make,
382 })(input),
383 runtimeRequest: (input) =>
384 randomId<RuntimeRequestId, typeof input>({
385 kind: "runtime_request",
386 prefix: "runtime-request",
387 parts: [
388 "provider",
389 input.driver,
390 ...(input.providerTurnId === undefined ? [] : ["provider-turn", input.providerTurnId]),
391 ...(input.nativeRequestId === undefined
392 ? []
393 : ["native-request", input.nativeRequestId]),
394 ],
395 make: RuntimeRequestId.make,
396 })(input),
397 checkpointScope: (input) =>
398 Effect.succeed(
399 CheckpointScopeId.make(
400 joinId("checkpoint-scope", "thread", input.threadId, "name", input.name),
401 ),
402 ),
403 checkpoint: (input) =>
404 Effect.succeed(
405 CheckpointId.make(
406 joinId("checkpoint", "scope", input.checkpointScopeId, "name", input.name),
407 ),
408 ),
409 contextHandoff: (input) =>
410 randomId<ContextHandoffId, typeof input>({
411 kind: "context_handoff",
412 prefix: "context-handoff",
413 parts: [
414 "thread",
415 input.threadId,
416 "from-provider-instance",
417 input.fromProviderInstanceId,
418 "to-provider-instance",
419 input.toProviderInstanceId,
420 ],
421 make: ContextHandoffId.make,
422 })(input),
423 contextTransfer: (input) =>
424 randomId<ContextTransferId, typeof input>({
425 kind: "context_transfer",
426 prefix: "context-transfer",
427 parts: [
428 "type",
429 input.type,
430 "source-thread",
431 input.sourceThreadId,
432 "target-thread",
433 input.targetThreadId,
434 ],
435 make: ContextTransferId.make,
436 })(input),
437 plan: (input) =>
438 randomId<PlanId, typeof input>({
439 kind: "plan",
440 prefix: "plan",
441 parts: [
442 "thread",
443 input.threadId,
444 "provider",
445 input.driver,
446 ...(input.runId === undefined ? [] : ["run", input.runId]),
447 ],
448 make: PlanId.make,
449 })(input),
450 },
451 derive,
452 }),
453);
454
455/** For adapters: subagent threads already stored under a legacy derived id keep that id. */
456export const layerWithLegacySubagentThreads = (
457 legacyThreadIds: ReadonlySet<string>,
458): Layer.Layer<IdAllocatorV2> =>
459 Layer.effect(
460 IdAllocatorV2,
461 Effect.gen(function* () {
462 const base = yield* IdAllocatorV2;
463 return IdAllocatorV2.of({
464 allocate: base.allocate,
465 derive: { ...base.derive, subagentThread: makeDeriveSubagentThread(legacyThreadIds) },
466 });
467 }),
468 ).pipe(Layer.provide(layer));