| 1 | import type { ProviderApprovalDecision } from "@t3tools/contracts"; |
| 2 | import * as Schema from "effect/Schema"; |
| 3 | |
| 4 | const NonEmptyString = Schema.String.check(Schema.isMinLength(1)); |
| 5 | |
| 6 | const MuseUsage = Schema.Struct({ |
| 7 | inputTokens: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)), |
| 8 | outputTokens: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)), |
| 9 | cachedTokens: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)), |
| 10 | reasoningTokens: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)), |
| 11 | cacheReadTokens: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))), |
| 12 | cacheWriteTokens: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))), |
| 13 | }); |
| 14 | |
| 15 | export const MuseItem = Schema.Struct({ |
| 16 | /** `workflow`: the launched entry's name. */ |
| 17 | entryId: Schema.optional(Schema.String), |
| 18 | /** `workflow`: each child agent's latest attempt, re-sent whole on every change. */ |
| 19 | children: Schema.optional( |
| 20 | Schema.Array( |
| 21 | Schema.Struct({ |
| 22 | childId: NonEmptyString, |
| 23 | attempt: Schema.Int, |
| 24 | status: Schema.String, |
| 25 | label: Schema.optional(Schema.String), |
| 26 | terminal: Schema.optional(Schema.String), |
| 27 | durationMs: Schema.optional(Schema.Finite), |
| 28 | /** Present on a failed attempt, even one whose `terminal` is `completed`. */ |
| 29 | failureKind: Schema.optional(Schema.String), |
| 30 | failureReason: Schema.optional(Schema.String), |
| 31 | }), |
| 32 | ), |
| 33 | ), |
| 34 | /** `workflow`: the run's result, once it settles. */ |
| 35 | finalSummary: Schema.optional(Schema.String), |
| 36 | itemId: NonEmptyString, |
| 37 | kind: NonEmptyString, |
| 38 | revision: Schema.Int.check(Schema.isGreaterThan(0)), |
| 39 | status: NonEmptyString, |
| 40 | turnId: Schema.optional(Schema.NullOr(Schema.String)), |
| 41 | text: Schema.optional(Schema.String), |
| 42 | summary: Schema.optional(Schema.Array(Schema.String)), |
| 43 | tool: Schema.optional(Schema.String), |
| 44 | args: Schema.optional(Schema.String), |
| 45 | visibleOutput: Schema.optional(Schema.String), |
| 46 | truncated: Schema.optional(Schema.Boolean), |
| 47 | failureReason: Schema.optional(Schema.String), |
| 48 | fallbackText: Schema.optional(Schema.String), |
| 49 | commandText: Schema.optional(Schema.String), |
| 50 | exitCode: Schema.optional(Schema.Int), |
| 51 | durationMs: Schema.optional(Schema.Finite), |
| 52 | subagentId: Schema.optional(Schema.String), |
| 53 | childSessionId: Schema.optional(Schema.String), |
| 54 | objective: Schema.optional(Schema.String), |
| 55 | role: Schema.optional(Schema.String), |
| 56 | controlStatus: Schema.optional(Schema.String), |
| 57 | result: Schema.optional( |
| 58 | Schema.Struct({ summary: Schema.String, text: Schema.optional(Schema.String) }), |
| 59 | ), |
| 60 | usage: Schema.optional(MuseUsage), |
| 61 | outcome: Schema.optional(Schema.String), |
| 62 | trigger: Schema.optional(Schema.String), |
| 63 | reason: Schema.optional(Schema.String), |
| 64 | }); |
| 65 | export type MuseItem = typeof MuseItem.Type; |
| 66 | |
| 67 | export const MuseSessionResult = Schema.Struct({ |
| 68 | session: Schema.Struct({ |
| 69 | sessionId: NonEmptyString, |
| 70 | modelId: Schema.optional(Schema.NullOr(Schema.String)), |
| 71 | activeTurnId: Schema.optional(Schema.NullOr(Schema.String)), |
| 72 | }), |
| 73 | history: Schema.optional( |
| 74 | Schema.Struct({ |
| 75 | items: Schema.NullOr(Schema.Array(MuseItem)), |
| 76 | snapshot: Schema.optional( |
| 77 | Schema.NullOr( |
| 78 | Schema.Struct({ |
| 79 | schemaVersion: Schema.optional(Schema.Int), |
| 80 | state: Schema.Struct({ items: Schema.Array(MuseItem) }), |
| 81 | }), |
| 82 | ), |
| 83 | ), |
| 84 | }), |
| 85 | ), |
| 86 | }); |
| 87 | export const MuseTurnStartResult = Schema.Struct({ turnId: NonEmptyString }); |
| 88 | |
| 89 | export const MuseApproval = Schema.Struct({ |
| 90 | approvalId: NonEmptyString, |
| 91 | protectedWrite: Schema.optional(Schema.Boolean), |
| 92 | judgeEscalated: Schema.optional(Schema.Boolean), |
| 93 | sessionId: NonEmptyString, |
| 94 | turnId: Schema.optional(Schema.String), |
| 95 | itemId: Schema.optional(Schema.String), |
| 96 | toolName: Schema.optional(Schema.String), |
| 97 | rawArgs: Schema.optional(Schema.String), |
| 98 | /** Set when a workflow child asks; `turnId` is then the child's run, not a session turn. */ |
| 99 | subagentOrigin: Schema.optional(Schema.Struct({ subagentId: NonEmptyString })), |
| 100 | currentRequirementId: Schema.Struct({ approvalId: NonEmptyString, sourceIndex: Schema.Int }), |
| 101 | availableChoices: Schema.Array( |
| 102 | Schema.Struct({ |
| 103 | choiceId: NonEmptyString, |
| 104 | label: Schema.String, |
| 105 | decision: NonEmptyString, |
| 106 | scope: Schema.String, |
| 107 | }), |
| 108 | ), |
| 109 | subject: Schema.Struct({ |
| 110 | kind: Schema.String, |
| 111 | access: Schema.optional(Schema.String), |
| 112 | command: Schema.optional(Schema.String), |
| 113 | path: Schema.optional(Schema.String), |
| 114 | host: Schema.optional(Schema.String), |
| 115 | target: Schema.optional(Schema.String), |
| 116 | }), |
| 117 | }); |
| 118 | export type MuseApproval = typeof MuseApproval.Type; |
| 119 | |
| 120 | export const MuseUserInput = Schema.Struct({ |
| 121 | userInputId: NonEmptyString, |
| 122 | sessionId: NonEmptyString, |
| 123 | turnId: NonEmptyString, |
| 124 | questions: Schema.Array( |
| 125 | Schema.Struct({ |
| 126 | id: NonEmptyString, |
| 127 | header: Schema.String, |
| 128 | question: Schema.String, |
| 129 | options: Schema.Array( |
| 130 | Schema.Struct({ label: Schema.String, description: Schema.optional(Schema.String) }), |
| 131 | ), |
| 132 | selection: Schema.Struct({ |
| 133 | mode: Schema.Literals(["single", "multiple"]), |
| 134 | minSelections: Schema.optional(Schema.Int), |
| 135 | maxSelections: Schema.optional(Schema.Int), |
| 136 | }), |
| 137 | }), |
| 138 | ), |
| 139 | }); |
| 140 | export type MuseUserInput = typeof MuseUserInput.Type; |
| 141 | |
| 142 | export const MuseTokenUsageEvent = Schema.Struct({ |
| 143 | turnId: NonEmptyString, |
| 144 | promptTokens: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)), |
| 145 | usage: MuseUsage, |
| 146 | }); |
| 147 | export const MuseContextUsage = Schema.Struct({ |
| 148 | usedTokens: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)), |
| 149 | windowTokens: Schema.optional(Schema.Int.check(Schema.isGreaterThan(0))), |
| 150 | }); |
| 151 | export const MuseTodoList = Schema.Struct({ |
| 152 | items: Schema.Array(Schema.Struct({ text: Schema.String, status: Schema.String })), |
| 153 | }); |
| 154 | export const MuseTurnCompleted = Schema.Struct({ |
| 155 | turnId: NonEmptyString, |
| 156 | terminal: NonEmptyString, |
| 157 | reason: Schema.optional(Schema.String), |
| 158 | error: Schema.optional(Schema.Struct({ message: Schema.String })), |
| 159 | usage: Schema.optional(MuseUsage), |
| 160 | }); |
| 161 | export const MuseTurnRetryScheduled = Schema.Struct({ |
| 162 | turnId: NonEmptyString, |
| 163 | nextAttempt: Schema.Int.check(Schema.isGreaterThan(0)), |
| 164 | maxAttempts: Schema.Int.check(Schema.isGreaterThan(0)), |
| 165 | reason: NonEmptyString, |
| 166 | retryDelayMs: Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0)), |
| 167 | }); |
| 168 | export const MuseCompactResult = Schema.Struct({ |
| 169 | status: Schema.String, |
| 170 | reason: Schema.optional(Schema.String), |
| 171 | }); |
| 172 | |
| 173 | export const MuseItemEvent = Schema.Struct({ item: MuseItem }); |
| 174 | export const MuseDelta = Schema.Struct({ |
| 175 | itemId: NonEmptyString, |
| 176 | delta: Schema.String, |
| 177 | field: Schema.optional(Schema.String), |
| 178 | }); |
| 179 | |
| 180 | /** The T3 decision for one native Muse choice, or none when T3 has no equivalent. */ |
| 181 | export function museApprovalDecision( |
| 182 | choice: Pick<MuseApproval["availableChoices"][number], "decision" | "scope">, |
| 183 | ): ProviderApprovalDecision | undefined { |
| 184 | switch (choice.decision) { |
| 185 | case "approved": |
| 186 | return "accept"; |
| 187 | case "approvedForSession": |
| 188 | return "acceptForSession"; |
| 189 | case "approvedPolicyAmendment": |
| 190 | return choice.scope === "session" |
| 191 | ? "acceptForSession" |
| 192 | : choice.scope === "localPersistent" |
| 193 | ? "acceptAlways" |
| 194 | : undefined; |
| 195 | case "denied": |
| 196 | case "deniedPolicyAmendment": |
| 197 | return "decline"; |
| 198 | case "abort": |
| 199 | return "cancel"; |
| 200 | default: |
| 201 | return undefined; |
| 202 | } |
| 203 | } |
| 204 | |
| 205 | /** |
| 206 | * Maps each canonical decision to one native choice. Muse shell approvals offer |
| 207 | * no `denied` choice, only `abort` ("Reject"), so `decline` falls back to it. |
| 208 | */ |
| 209 | export function museApprovalChoices(approval: Pick<MuseApproval, "availableChoices">) { |
| 210 | const choices = new Map<ProviderApprovalDecision, MuseApproval["availableChoices"][number]>(); |
| 211 | const rank = (choice: MuseApproval["availableChoices"][number]) => |
| 212 | (choice.scope === "once" ? 0 : choice.scope === "session" ? 2 : 4) + |
| 213 | (choice.decision.endsWith("PolicyAmendment") ? 1 : 0); |
| 214 | for (const choice of approval.availableChoices) { |
| 215 | const decision = museApprovalDecision(choice); |
| 216 | if (!decision) continue; |
| 217 | const previous = choices.get(decision); |
| 218 | if (!previous || rank(choice) < rank(previous)) choices.set(decision, choice); |
| 219 | } |
| 220 | const abort = choices.get("cancel"); |
| 221 | if (!choices.has("decline") && abort) choices.set("decline", abort); |
| 222 | return choices; |
| 223 | } |
| 224 | |
| 225 | /** The approval buttons to show: one per native choice, preferring `decline` over `cancel`. */ |
| 226 | export function museApprovalOptions(approval: Pick<MuseApproval, "availableChoices">) { |
| 227 | const choices = museApprovalChoices(approval); |
| 228 | const declineChoiceId = choices.get("decline")?.choiceId; |
| 229 | return [...choices] |
| 230 | .filter(([decision, choice]) => decision !== "cancel" || choice.choiceId !== declineChoiceId) |
| 231 | .map(([decision, choice]) => ({ decision, label: choice.label })); |
| 232 | } |