| ... | ... | @@ -18,6 +18,7 @@ import { |
| 18 | 18 | type ProviderInstanceId, |
| 19 | 19 | type PlanId, |
| 20 | 20 | type OrchestrationV2PlanStep, |
| 21 | type ProviderApprovalDecision, |
| 21 | 22 | type RuntimeRequestId, |
| 22 | 23 | type ServerProviderModel, |
| 23 | 24 | } from "@t3tools/contracts"; |
| ... | ... | @@ -157,7 +158,7 @@ const MuseProviderCapabilitiesV2 = { |
| 157 | 158 | supportsApplyPatchApproval: false, |
| 158 | 159 | approvalsHaveNativeRequestIds: true, |
| 159 | 160 | approvalCallbacksAreLiveOnly: true, |
| 160 | | approvalsCanOriginateFromSubagents: false, |
| 161 | approvalsCanOriginateFromSubagents: true, |
| 161 | 162 | }, |
| 162 | 163 | planning: { |
| 163 | 164 | emitsPlanUpdated: true, |
| ... | ... | @@ -664,6 +665,8 @@ export function makeMuseAdapterV2(options: MuseAdapterV2Options): ProviderAdapte |
| 664 | 665 | } |
| 665 | 666 | observedChildren.clear(); |
| 666 | 667 | if (thread?.pendingBackgroundTasks?.length) yield* syncBackground(); |
| 668 | // Only workflow children's requests outlive a turn; they end with the workflow. |
| 669 | for (const entry of pending.values()) yield* resolvePending(entry, "cancelled"); |
| 667 | 670 | }); |
| 668 | 671 | const resolvePending = Effect.fnUntraced(function* ( |
| 669 | 672 | entry: PendingRequest, |
| ... | ... | @@ -718,7 +721,9 @@ export function makeMuseAdapterV2(options: MuseAdapterV2Options): ProviderAdapte |
| 718 | 721 | yield* publishItem(turn, item, item.status === "inProgress" ? status : undefined); |
| 719 | 722 | } |
| 720 | 723 | turn.dirty.clear(); |
| 721 | | for (const entry of pending.values()) yield* resolvePending(entry, "cancelled"); |
| 724 | // A workflow child's request is runless and keeps waiting after the turn ends. |
| 725 | for (const entry of pending.values()) |
| 726 | if (entry.request.providerTurnId !== null) yield* resolvePending(entry, "cancelled"); |
| 722 | 727 | turn.providerTurn = { ...turn.providerTurn, status, completedAt }; |
| 723 | 728 | lastProviderTurn = turn.providerTurn; |
| 724 | 729 | yield* emit({ |
| ... | ... | @@ -801,28 +806,54 @@ export function makeMuseAdapterV2(options: MuseAdapterV2Options): ProviderAdapte |
| 801 | 806 | }); |
| 802 | 807 | const owns = (turn: ActiveTurn, turnId: string) => |
| 803 | 808 | turnId === turn.nativeId || turn.joined.has(turnId); |
| 809 | /** Sends T3's decision for a pending approval to Muse. */ |
| 810 | const decideApproval = Effect.fnUntraced(function* ( |
| 811 | entry: PendingRequest, |
| 812 | decision: ProviderApprovalDecision | undefined, |
| 813 | ) { |
| 814 | if (entry.native.type !== "approval") return; |
| 815 | const approval = entry.native.value; |
| 816 | const choice = decision && museApprovalChoices(approval).get(decision); |
| 817 | if (!choice) return yield* protocolError("Muse did not offer this approval decision"); |
| 818 | // Recorded first: Muse can settle the approval before it acknowledges the command. |
| 819 | entry.response = { decision }; |
| 820 | yield* request("approval/decide", { |
| 821 | approvalId: approval.approvalId, |
| 822 | requirementId: approval.currentRequirementId, |
| 823 | choiceId: choice.choiceId, |
| 824 | }).pipe(Effect.tapError(() => Effect.sync(() => delete entry.response))); |
| 825 | }); |
| 826 | /** |
| 827 | * Shows a Muse approval or question as a T3 request. A workflow child's approval |
| 828 | * is runless: the turn that started the workflow may end long before the child |
| 829 | * asks, and T3 keeps a runless request on the thread until Muse settles it. |
| 830 | */ |
| 804 | 831 | const publishRequest = Effect.fnUntraced(function* (native: PendingRequest["native"]) { |
| 805 | | const turn = active; |
| 832 | const child = native.type === "approval" && native.value.subagentOrigin !== undefined; |
| 833 | const turn = child ? undefined : active; |
| 806 | 834 | if ( |
| 807 | | !turn || |
| 835 | !thread || |
| 836 | (!child && !turn) || |
| 808 | 837 | native.value.sessionId !== nativeSessionId || |
| 809 | | (native.value.turnId && !owns(turn, native.value.turnId)) |
| 838 | (turn && native.value.turnId && !owns(turn, native.value.turnId)) |
| 810 | 839 | ) |
| 811 | 840 | return; |
| 812 | 841 | const nativeId = |
| 813 | 842 | native.type === "approval" ? native.value.approvalId : native.value.userInputId; |
| 814 | | if (turn.settledRequests.has(`${native.type}:${nativeId}`)) return; |
| 843 | if (turn?.settledRequests.has(`${native.type}:${nativeId}`)) return; |
| 815 | 844 | const previous = [...pending.values()].find( |
| 816 | 845 | (entry) => |
| 817 | 846 | entry.native.type === native.type && |
| 818 | 847 | entry.request.nativeRequestRef?.nativeId === nativeId, |
| 819 | 848 | ); |
| 849 | const providerTurnId = turn?.providerTurn.id ?? null; |
| 850 | const threadId = turn?.input.threadId ?? thread.appThreadId ?? input.threadId; |
| 820 | 851 | const requestId = |
| 821 | 852 | previous?.request.id ?? |
| 822 | 853 | (yield* idAllocator.allocate |
| 823 | 854 | .runtimeRequest({ |
| 824 | 855 | driver: MUSE_PROVIDER, |
| 825 | | providerTurnId: turn.providerTurn.id, |
| 856 | ...(providerTurnId ? { providerTurnId } : {}), |
| 826 | 857 | nativeRequestId: nativeId, |
| 827 | 858 | }) |
| 828 | 859 | .pipe( |
| ... | ... | @@ -843,7 +874,7 @@ export function makeMuseAdapterV2(options: MuseAdapterV2Options): ProviderAdapte |
| 843 | 874 | const runtimeRequest: OrchestrationV2RuntimeRequest = { |
| 844 | 875 | id: requestId, |
| 845 | 876 | nodeId, |
| 846 | | providerTurnId: turn.providerTurn.id, |
| 877 | providerTurnId, |
| 847 | 878 | nativeRequestRef: nativeRef(nativeId), |
| 848 | 879 | kind, |
| 849 | 880 | status: "pending", |
| ... | ... | @@ -853,23 +884,37 @@ export function makeMuseAdapterV2(options: MuseAdapterV2Options): ProviderAdapte |
| 853 | 884 | }; |
| 854 | 885 | const node: OrchestrationV2ExecutionNode = { |
| 855 | 886 | id: nodeId, |
| 856 | | threadId: turn.input.threadId, |
| 857 | | runId: turn.input.runId, |
| 858 | | parentNodeId: turn.input.rootNodeId, |
| 859 | | rootNodeId: turn.input.rootNodeId, |
| 887 | threadId, |
| 888 | runId: turn?.input.runId ?? null, |
| 889 | parentNodeId: turn?.input.rootNodeId ?? null, |
| 890 | rootNodeId: turn?.input.rootNodeId ?? nodeId, |
| 860 | 891 | kind: native.type === "approval" ? "approval_request" : "user_input_request", |
| 861 | 892 | status: "waiting", |
| 862 | 893 | countsForRun: false, |
| 863 | | providerThreadId: turn.input.providerThread.id, |
| 864 | | providerTurnId: turn.providerTurn.id, |
| 894 | providerThreadId: thread.id, |
| 895 | providerTurnId, |
| 865 | 896 | nativeItemRef: nativeRef(nativeId), |
| 866 | 897 | runtimeRequestId: requestId, |
| 867 | 898 | checkpointScopeId: null, |
| 868 | 899 | startedAt: runtimeRequest.createdAt, |
| 869 | 900 | completedAt: null, |
| 870 | 901 | }; |
| 902 | const latestTurn = active?.providerTurn ?? lastProviderTurn; |
| 871 | 903 | const base = { |
| 872 | | ...baseItem(turn, `request:${nativeId}`, time), |
| 904 | ...(turn |
| 905 | ? baseItem(turn, `request:${nativeId}`, time) |
| 906 | : { |
| 907 | threadId, |
| 908 | runId: null, |
| 909 | providerThreadId: thread.id, |
| 910 | providerTurnId: null, |
| 911 | nativeItemRef: nativeRef(nativeId), |
| 912 | parentItemId: null, |
| 913 | // Sorts after the latest turn's items. |
| 914 | ordinal: previous?.item.ordinal ?? ((latestTurn?.ordinal ?? 0) + 1) * 100, |
| 915 | startedAt: runtimeRequest.createdAt, |
| 916 | updatedAt: time, |
| 917 | }), |
| 873 | 918 | id: idAllocator.derive.approvalTurnItem({ requestId }), |
| 874 | 919 | nodeId, |
| 875 | 920 | status: "waiting" as const, |
| ... | ... | @@ -909,16 +954,71 @@ export function makeMuseAdapterV2(options: MuseAdapterV2Options): ProviderAdapte |
| 909 | 954 | required: true, |
| 910 | 955 | })), |
| 911 | 956 | }; |
| 912 | | pending.set(requestId, { native, request: runtimeRequest, node, item }); |
| 957 | const entry: PendingRequest = { native, request: runtimeRequest, node, item }; |
| 958 | pending.set(requestId, entry); |
| 913 | 959 | yield* emit({ |
| 914 | 960 | type: "runtime_request.updated", |
| 915 | 961 | driver: MUSE_PROVIDER, |
| 916 | | threadId: turn.input.threadId, |
| 962 | threadId, |
| 917 | 963 | runtimeRequest, |
| 918 | 964 | }); |
| 919 | 965 | yield* emit({ type: "node.updated", driver: MUSE_PROVIDER, node }); |
| 920 | 966 | yield* emit({ type: "turn_item.updated", driver: MUSE_PROVIDER, turnItem: item }); |
| 921 | 967 | yield* updateSession("waiting"); |
| 968 | // Muse does not give workflow children the session's allowAll mode, so in full |
| 969 | // access T3 approves for them. If Muse rejects that, the request waits for the user. |
| 970 | if (child && input.runtimePolicy.runtimeMode === "full-access") |
| 971 | yield* decideApproval(entry, "accept").pipe(Effect.ignore, Effect.forkIn(scope)); |
| 972 | }); |
| 973 | /** Approval and question events; `turn` is undefined for a workflow child's approval. */ |
| 974 | const handleRequestEvent = Effect.fnUntraced(function* ( |
| 975 | method: string, |
| 976 | params: Record<string, unknown>, |
| 977 | turn: ActiveTurn | undefined, |
| 978 | ) { |
| 979 | switch (method) { |
| 980 | case "approval/requested": |
| 981 | case "approval/updated": |
| 982 | if ( |
| 983 | method === "approval/updated" && |
| 984 | ![...pending.values()].some( |
| 985 | (entry) => |
| 986 | entry.native.type === "approval" && |
| 987 | entry.native.value.approvalId === params.approvalId, |
| 988 | ) |
| 989 | ) |
| 990 | return; |
| 991 | return yield* publishRequest({ |
| 992 | type: "approval", |
| 993 | value: yield* decode(MuseApproval, params), |
| 994 | }); |
| 995 | case "userInput/requested": |
| 996 | return yield* publishRequest({ |
| 997 | type: "question", |
| 998 | value: yield* decode(MuseUserInput, params), |
| 999 | }); |
| 1000 | case "approval/resolved": |
| 1001 | case "userInput/settled": { |
| 1002 | const id = method === "approval/resolved" ? params.approvalId : params.userInputId; |
| 1003 | if (typeof id === "string") |
| 1004 | turn?.settledRequests.add( |
| 1005 | `${method === "approval/resolved" ? "approval" : "question"}:${id}`, |
| 1006 | ); |
| 1007 | const entry = [...pending.values()].find( |
| 1008 | (candidate) => candidate.request.nativeRequestRef?.nativeId === id, |
| 1009 | ); |
| 1010 | if (entry) { |
| 1011 | // An approval settled outside T3 still shows Muse's own decision. |
| 1012 | const native = |
| 1013 | method === "approval/resolved" && typeof params.decision === "string" |
| 1014 | ? museApprovalDecision({ decision: params.decision, scope: "once" }) |
| 1015 | : undefined; |
| 1016 | if (!entry.response && native) entry.response = { decision: native }; |
| 1017 | yield* resolvePending(entry, "resolved"); |
| 1018 | } |
| 1019 | if (!pending.size) yield* updateSession(active ? "running" : "ready"); |
| 1020 | } |
| 1021 | } |
| 922 | 1022 | }); |
| 923 | 1023 | const handleNotification = Effect.fnUntraced(function* (method: string, data: unknown) { |
| 924 | 1024 | const params = yield* decode(recordSchema, data); |
| ... | ... | @@ -962,6 +1062,10 @@ export function makeMuseAdapterV2(options: MuseAdapterV2Options): ProviderAdapte |
| 962 | 1062 | return; |
| 963 | 1063 | } |
| 964 | 1064 | } |
| 1065 | // A workflow child's approval comes under the child's own run id, during a turn |
| 1066 | // or after it, so it skips turn routing. |
| 1067 | if (params.subagentOrigin !== undefined && method.startsWith("approval/")) |
| 1068 | return yield* handleRequestEvent(method, params, undefined); |
| 965 | 1069 | // Muse starts a turn on its own when a workflow finishes, to report its result. |
| 966 | 1070 | // During a run, that run shows it. Otherwise hold it and ask the orchestrator for |
| 967 | 1071 | // a run; that run takes the held events, unless a user turn takes them first. |
| ... | ... | @@ -1097,45 +1201,11 @@ export function makeMuseAdapterV2(options: MuseAdapterV2Options): ProviderAdapte |
| 1097 | 1201 | } |
| 1098 | 1202 | case "approval/requested": |
| 1099 | 1203 | case "approval/updated": |
| 1100 | | if ( |
| 1101 | | method === "approval/updated" && |
| 1102 | | ![...pending.values()].some( |
| 1103 | | (entry) => |
| 1104 | | entry.native.type === "approval" && |
| 1105 | | entry.native.value.approvalId === params.approvalId, |
| 1106 | | ) |
| 1107 | | ) |
| 1108 | | break; |
| 1109 | | yield* publishRequest({ type: "approval", value: yield* decode(MuseApproval, params) }); |
| 1110 | | break; |
| 1111 | | case "userInput/requested": |
| 1112 | | yield* publishRequest({ |
| 1113 | | type: "question", |
| 1114 | | value: yield* decode(MuseUserInput, params), |
| 1115 | | }); |
| 1116 | | break; |
| 1117 | 1204 | case "approval/resolved": |
| 1118 | | case "userInput/settled": { |
| 1119 | | const id = method === "approval/resolved" ? params.approvalId : params.userInputId; |
| 1120 | | if (typeof id === "string") |
| 1121 | | turn.settledRequests.add( |
| 1122 | | `${method === "approval/resolved" ? "approval" : "question"}:${id}`, |
| 1123 | | ); |
| 1124 | | const entry = [...pending.values()].find( |
| 1125 | | (candidate) => candidate.request.nativeRequestRef?.nativeId === id, |
| 1126 | | ); |
| 1127 | | if (entry) { |
| 1128 | | // An approval settled outside T3 still shows Muse's own decision. |
| 1129 | | const native = |
| 1130 | | method === "approval/resolved" && typeof params.decision === "string" |
| 1131 | | ? museApprovalDecision({ decision: params.decision, scope: "once" }) |
| 1132 | | : undefined; |
| 1133 | | if (!entry.response && native) entry.response = { decision: native }; |
| 1134 | | yield* resolvePending(entry, "resolved"); |
| 1135 | | } |
| 1136 | | if (!pending.size) yield* updateSession("running"); |
| 1205 | case "userInput/requested": |
| 1206 | case "userInput/settled": |
| 1207 | yield* handleRequestEvent(method, params, turn); |
| 1137 | 1208 | break; |
| 1138 | | } |
| 1139 | 1209 | case "session/tokenUsage": { |
| 1140 | 1210 | const usage = yield* decode(MuseTokenUsageEvent, params); |
| 1141 | 1211 | const previous = turn.providerTurn.turnTokenUsage; |
| ... | ... | @@ -1892,18 +1962,8 @@ export function makeMuseAdapterV2(options: MuseAdapterV2Options): ProviderAdapte |
| 1892 | 1962 | Effect.gen(function* () { |
| 1893 | 1963 | const entry = pending.get(args.requestId); |
| 1894 | 1964 | if (!entry) return yield* protocolError("This Muse request is no longer pending"); |
| 1895 | | if (entry.native.type === "approval") { |
| 1896 | | const approval = entry.native.value; |
| 1897 | | const choice = args.decision && museApprovalChoices(approval).get(args.decision); |
| 1898 | | if (!choice) return yield* protocolError("Muse did not offer this approval decision"); |
| 1899 | | // Recorded first: Muse can settle the approval before it acknowledges the command. |
| 1900 | | entry.response = { decision: args.decision }; |
| 1901 | | yield* request("approval/decide", { |
| 1902 | | approvalId: approval.approvalId, |
| 1903 | | requirementId: approval.currentRequirementId, |
| 1904 | | choiceId: choice.choiceId, |
| 1905 | | }).pipe(Effect.tapError(() => Effect.sync(() => delete entry.response))); |
| 1906 | | } else { |
| 1965 | if (entry.native.type === "approval") yield* decideApproval(entry, args.decision); |
| 1966 | else { |
| 1907 | 1967 | const questionRequest = entry.native.value; |
| 1908 | 1968 | const answers = []; |
| 1909 | 1969 | for (const question of questionRequest.questions) { |