| 1 | import type { |
| 2 | ModelSelection, |
| 3 | OrchestrationV2ProviderCapabilities, |
| 4 | ProviderDriverKind, |
| 5 | ProviderInteractionMode, |
| 6 | RuntimeMode, |
| 7 | } from "@t3tools/contracts"; |
| 8 | import { modelSelectionsEqual } from "@t3tools/shared/model"; |
| 9 | |
| 10 | import type { ProviderContinuationIdentity } from "@t3tools/provider-core/server/driver"; |
| 11 | import type { ProviderSelectionTransitionPlan } from "@t3tools/provider-core/server/selectionTransition"; |
| 12 | |
| 13 | export 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 | |
| 20 | export 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 | |
| 30 | export interface ProviderSessionTransitionTarget extends ProviderSessionTransitionState { |
| 31 | readonly available: boolean; |
| 32 | } |
| 33 | |
| 34 | export 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 | } |