1import type {
2 ModelSelection,
3 OrchestrationV2ProviderCapabilities,
4 ProviderDriverKind,
5 ProviderInteractionMode,
6 RuntimeMode,
7} from "@t3tools/contracts";
8import { modelSelectionsEqual } from "@t3tools/shared/model";
9
10import type { ProviderContinuationIdentity } from "@t3tools/provider-core/server/driver";
11import type { ProviderSelectionTransitionPlan } from "@t3tools/provider-core/server/selectionTransition";
12
13export type ProviderSessionTransition =
14 | { readonly type: "reuse" }
15 | { readonly type: "switch_model_in_session" }
16 | { readonly type: "restart_and_resume" }
17 | { readonly type: "create_with_handoff" }
18 | { readonly type: "reject"; readonly reason: string };
19
20export interface ProviderSessionTransitionState {
21 readonly driver: ProviderDriverKind;
22 readonly continuationIdentity: ProviderContinuationIdentity;
23 readonly modelSelection: ModelSelection;
24 readonly runtimeMode: RuntimeMode;
25 readonly interactionMode: ProviderInteractionMode;
26 readonly workspace: string;
27 readonly capabilities: OrchestrationV2ProviderCapabilities;
28}
29
30export interface ProviderSessionTransitionTarget extends ProviderSessionTransitionState {
31 readonly available: boolean;
32}
33
34export function decideProviderSessionTransition(input: {
35 readonly current: ProviderSessionTransitionState | null;
36 readonly target: ProviderSessionTransitionTarget;
37 readonly selectionTransition?: ProviderSelectionTransitionPlan;
38}): ProviderSessionTransition {
39 if (!input.target.available) {
40 return { type: "reject", reason: "The target provider instance is unavailable." };
41 }
42 if (input.current === null) {
43 return { type: "create_with_handoff" };
44 }
45
46 const current = input.current;
47 const target = input.target;
48 const continuationCompatible =
49 current.continuationIdentity.driverKind === target.continuationIdentity.driverKind &&
50 current.continuationIdentity.continuationKey === target.continuationIdentity.continuationKey;
51 if (!continuationCompatible || current.driver !== target.driver) {
52 return { type: "create_with_handoff" };
53 }
54
55 const instanceChanged = current.modelSelection.instanceId !== target.modelSelection.instanceId;
56 const runtimeChanged = current.runtimeMode !== target.runtimeMode;
57 const workspaceChanged = current.workspace !== target.workspace;
58 const selectionChanged = !modelSelectionsEqual(current.modelSelection, target.modelSelection);
59 if (selectionChanged && !instanceChanged) {
60 switch (input.selectionTransition?.type) {
61 case "create_with_handoff":
62 return { type: "create_with_handoff" };
63 case "reject":
64 return { type: "reject", reason: input.selectionTransition.reason };
65 case undefined:
66 return {
67 type: "reject",
68 reason: "The provider adapter did not classify the selection change.",
69 };
70 case "apply_on_next_turn":
71 case "restart_session":
72 break;
73 }
74 }
75 if (instanceChanged || runtimeChanged || workspaceChanged) {
76 return { type: "restart_and_resume" };
77 }
78
79 if (selectionChanged) {
80 switch (input.selectionTransition?.type) {
81 case "apply_on_next_turn":
82 return { type: "switch_model_in_session" };
83 case "restart_session":
84 return { type: "restart_and_resume" };
85 case "create_with_handoff":
86 return { type: "create_with_handoff" };
87 case "reject":
88 return { type: "reject", reason: input.selectionTransition.reason };
89 case undefined:
90 return { type: "restart_and_resume" };
91 }
92 }
93
94 // Interaction mode is turn-scoped and is applied when the next turn starts.
95 return { type: "reuse" };
96}