1import type { ProviderApprovalDecision } from "@t3tools/contracts";
2import * as Schema from "effect/Schema";
3
4const NonEmptyString = Schema.String.check(Schema.isMinLength(1));
5
6const 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
15export 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});
65export type MuseItem = typeof MuseItem.Type;
66
67export 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});
87export const MuseTurnStartResult = Schema.Struct({ turnId: NonEmptyString });
88
89export 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});
118export type MuseApproval = typeof MuseApproval.Type;
119
120export 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});
140export type MuseUserInput = typeof MuseUserInput.Type;
141
142export const MuseTokenUsageEvent = Schema.Struct({
143 turnId: NonEmptyString,
144 promptTokens: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
145 usage: MuseUsage,
146});
147export const MuseContextUsage = Schema.Struct({
148 usedTokens: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
149 windowTokens: Schema.optional(Schema.Int.check(Schema.isGreaterThan(0))),
150});
151export const MuseTodoList = Schema.Struct({
152 items: Schema.Array(Schema.Struct({ text: Schema.String, status: Schema.String })),
153});
154export 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});
161export 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});
168export const MuseCompactResult = Schema.Struct({
169 status: Schema.String,
170 reason: Schema.optional(Schema.String),
171});
172
173export const MuseItemEvent = Schema.Struct({ item: MuseItem });
174export 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. */
181export 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 */
209export 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`. */
226export 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}