1import type {
2 ModelSelection,
3 OrchestrationV2Run,
4 OrchestrationV2ThreadProjection,
5 OrchestrationV2TurnItem,
6 ProviderInstanceId,
7 ServerProvider,
8} from "@t3tools/contracts";
9
10/**
11 * The subset of a projection run that handoff rows read. Kept minimal so the
12 * timeline can hold a content-stable snapshot: run status/timestamps churn on
13 * every stream event, but these fields only change when a run is added.
14 */
15export type HandoffTimelineRun = Pick<
16 OrchestrationV2Run,
17 "id" | "ordinal" | "providerInstanceId" | "modelSelection"
18>;
19
20export type HandoffEndpoints = ReturnType<typeof resolveHandoffEndpoints>;
21
22/** Preview the next send's handoff without adding anything to durable history. */
23export function resolvePendingHandoff(
24 projection: OrchestrationV2ThreadProjection | null | undefined,
25 selection: ModelSelection | null | undefined,
26 providers: ReadonlyArray<ServerProvider>,
27): HandoffEndpoints | null {
28 const from = projection?.runs.findLast(
29 (run) => run.startedAt !== null && run.status !== "rolled_back",
30 )?.modelSelection;
31 if (from === undefined || selection == null) return null;
32 const source = providers.find((provider) => provider.instanceId === from.instanceId);
33 const target = providers.find((provider) => provider.instanceId === selection.instanceId);
34 const needsHandoff =
35 from.instanceId === selection.instanceId
36 ? from.model !== selection.model && target?.requiresNewThreadForModelChange === true
37 : // Accounts sharing a continuation group resume the same native thread.
38 source?.continuation?.groupKey === undefined ||
39 source.driver !== target?.driver ||
40 source.continuation.groupKey !== target.continuation?.groupKey;
41 return needsHandoff ? { from: [from], to: selection } : null;
42}
43
44export function resolveHandoffEndpoints(
45 item: Pick<
46 Extract<OrchestrationV2TurnItem, { type: "handoff" }>,
47 "runId" | "fromModelSelections" | "fromProviderInstanceIds" | "toModel" | "toProviderInstanceId"
48 >,
49 runs: ReadonlyArray<HandoffTimelineRun>,
50) {
51 // Items persisted before models were stamped only carry instance ids;
52 // recover the models from the thread's runs (the handoff's own run is
53 // the target, the newest earlier run per source instance is the origin).
54 // Clients fall back to the provider display name when neither source has a model.
55 const handoffRun = item.runId === null ? undefined : runs.find((run) => run.id === item.runId);
56 const toModel =
57 item.toModel ??
58 (handoffRun !== undefined && handoffRun.providerInstanceId === item.toProviderInstanceId
59 ? handoffRun.modelSelection.model
60 : undefined);
61 const fromEndpoints: ReadonlyArray<{
62 readonly instanceId: ProviderInstanceId;
63 readonly model?: string | undefined;
64 }> =
65 item.fromModelSelections !== undefined && item.fromModelSelections.length > 0
66 ? item.fromModelSelections
67 : item.fromProviderInstanceIds.map((instanceId) => ({
68 instanceId,
69 model: latestRunModelBefore(runs, instanceId, handoffRun?.ordinal),
70 }));
71 return { from: fromEndpoints, to: { instanceId: item.toProviderInstanceId, model: toModel } };
72}
73
74/**
75 * Model of the newest run for `instanceId` that started before the handoff's
76 * own run. Legacy handoff items don't record their source models, but the
77 * covered runs are still in the projection.
78 */
79function latestRunModelBefore(
80 runs: ReadonlyArray<HandoffTimelineRun>,
81 instanceId: ProviderInstanceId,
82 beforeOrdinal: number | undefined,
83): string | undefined {
84 let latest: HandoffTimelineRun | undefined;
85 for (const run of runs) {
86 if (run.providerInstanceId !== instanceId) continue;
87 if (beforeOrdinal !== undefined && run.ordinal >= beforeOrdinal) continue;
88 if (latest === undefined || run.ordinal > latest.ordinal) latest = run;
89 }
90 return latest?.modelSelection.model;
91}