@truefoundry/assistant-ui-runtime 0.1.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +567 -0
- package/dist/index.d.ts +300 -0
- package/dist/index.js +4440 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -0
- package/src/agentSessionModule.d.ts +25 -0
- package/src/agentSpec.ts +44 -0
- package/src/askUserQuestion.ts +37 -0
- package/src/attachmentAdapter.test.ts +56 -0
- package/src/attachmentAdapter.ts +58 -0
- package/src/bindDraftAgentSession.test.ts +55 -0
- package/src/bindDraftAgentSession.ts +66 -0
- package/src/buildEditedUserMessageContent.test.ts +61 -0
- package/src/collectPending.test.ts +69 -0
- package/src/collectPending.ts +165 -0
- package/src/constants.ts +2 -0
- package/src/convertTurnMessages.test.ts +1991 -0
- package/src/convertTurnMessages.ts +1251 -0
- package/src/createSubAgent.ts +8 -0
- package/src/draftAgentConfig.test.ts +88 -0
- package/src/draftSessionBridge.ts +28 -0
- package/src/extractTurnUserText.ts +21 -0
- package/src/foldPeerThreads.test.ts +386 -0
- package/src/foldPeerThreads.ts +587 -0
- package/src/hooks.ts +123 -0
- package/src/index.ts +68 -0
- package/src/lastUserMessageText.ts +19 -0
- package/src/loadSessionSnapshot.test.ts +57 -0
- package/src/loadSessionSnapshot.ts +35 -0
- package/src/mcpAuth.ts +44 -0
- package/src/messageCustomMetadata.ts +37 -0
- package/src/modelMessageContent.ts +135 -0
- package/src/modelMessageImageContent.test.ts +109 -0
- package/src/modelMessageImageContent.ts +193 -0
- package/src/requiredActionInputs.test.ts +394 -0
- package/src/requiredActionInputs.ts +65 -0
- package/src/requiredActionsFromActiveUpdate.test.ts +95 -0
- package/src/sessionListStartTimestamp.ts +6 -0
- package/src/sessionSnapshot.ts +107 -0
- package/src/sessions.ts +36 -0
- package/src/streamTurn.test.ts +243 -0
- package/src/streamTurn.ts +119 -0
- package/src/toolApproval.test.ts +137 -0
- package/src/toolApproval.ts +459 -0
- package/src/toolResponse.test.ts +136 -0
- package/src/toolResponse.ts +427 -0
- package/src/truefoundryDraftThreadListAdapter.test.ts +140 -0
- package/src/truefoundryDraftThreadListAdapter.ts +63 -0
- package/src/truefoundryExtras.ts +45 -0
- package/src/truefoundryThreadListAdapter.test.ts +103 -0
- package/src/truefoundryThreadListAdapter.ts +59 -0
- package/src/turnEventHelpers.ts +98 -0
- package/src/turnStreamUpdate.ts +11 -0
- package/src/types.ts +92 -0
- package/src/useDraftAgentSpec.ts +173 -0
- package/src/useTrueFoundryAgentMessages.test.tsx +723 -0
- package/src/useTrueFoundryAgentMessages.ts +757 -0
- package/src/useTrueFoundryAgentRuntime.ts +268 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
applyApprovalDecisionsToMessage,
|
|
5
|
+
collectApprovalInputs,
|
|
6
|
+
messageHasPendingApprovals,
|
|
7
|
+
} from "./toolApproval.js";
|
|
8
|
+
|
|
9
|
+
describe("toolApproval", () => {
|
|
10
|
+
it("detects pending nested approvals", () => {
|
|
11
|
+
const message = {
|
|
12
|
+
id: "a1",
|
|
13
|
+
role: "assistant" as const,
|
|
14
|
+
content: [
|
|
15
|
+
{
|
|
16
|
+
type: "tool-call" as const,
|
|
17
|
+
toolCallId: "tc-root",
|
|
18
|
+
toolName: "create_sub_agent",
|
|
19
|
+
args: {},
|
|
20
|
+
argsText: "{}",
|
|
21
|
+
messages: [
|
|
22
|
+
{
|
|
23
|
+
id: "sub",
|
|
24
|
+
role: "assistant" as const,
|
|
25
|
+
content: [
|
|
26
|
+
{
|
|
27
|
+
type: "tool-call" as const,
|
|
28
|
+
toolCallId: "tc-sub",
|
|
29
|
+
toolName: "bash",
|
|
30
|
+
args: {},
|
|
31
|
+
argsText: "{}",
|
|
32
|
+
approval: { id: "approval-sub" },
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
status: { type: "requires-action" as const, reason: "tool-calls" as const },
|
|
36
|
+
createdAt: new Date(),
|
|
37
|
+
metadata: {
|
|
38
|
+
unstable_state: null,
|
|
39
|
+
unstable_annotations: [],
|
|
40
|
+
unstable_data: [],
|
|
41
|
+
steps: [],
|
|
42
|
+
custom: {},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
status: { type: "requires-action" as const, reason: "tool-calls" as const },
|
|
49
|
+
createdAt: new Date(),
|
|
50
|
+
metadata: {
|
|
51
|
+
unstable_state: null,
|
|
52
|
+
unstable_annotations: [],
|
|
53
|
+
unstable_data: [],
|
|
54
|
+
steps: [],
|
|
55
|
+
custom: {},
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
expect(messageHasPendingApprovals(message)).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("collects decided approvals for sdk resume", () => {
|
|
63
|
+
const pending = {
|
|
64
|
+
id: "a1",
|
|
65
|
+
role: "assistant" as const,
|
|
66
|
+
content: [
|
|
67
|
+
{
|
|
68
|
+
type: "tool-call" as const,
|
|
69
|
+
toolCallId: "approval-1",
|
|
70
|
+
toolName: "bash",
|
|
71
|
+
args: {},
|
|
72
|
+
argsText: "{}",
|
|
73
|
+
approval: { id: "approval-1" },
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
status: { type: "requires-action" as const, reason: "tool-calls" as const },
|
|
77
|
+
createdAt: new Date(),
|
|
78
|
+
metadata: {
|
|
79
|
+
unstable_state: null,
|
|
80
|
+
unstable_annotations: [],
|
|
81
|
+
unstable_data: [],
|
|
82
|
+
steps: [],
|
|
83
|
+
custom: {},
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const decided = applyApprovalDecisionsToMessage(pending, {
|
|
88
|
+
approvalId: "approval-1",
|
|
89
|
+
approved: true,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(messageHasPendingApprovals(decided)).toBe(false);
|
|
93
|
+
const inputs = collectApprovalInputs(decided, "main");
|
|
94
|
+
expect(inputs).toEqual([
|
|
95
|
+
{
|
|
96
|
+
type: "user.tool_approval",
|
|
97
|
+
threadId: "main",
|
|
98
|
+
toolCallId: "approval-1",
|
|
99
|
+
approval: { status: "allow" },
|
|
100
|
+
},
|
|
101
|
+
]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("collects root approvals when custom thread id metadata is present", () => {
|
|
105
|
+
const pending = {
|
|
106
|
+
id: "turn-1-assistant",
|
|
107
|
+
role: "assistant" as const,
|
|
108
|
+
content: [
|
|
109
|
+
{
|
|
110
|
+
type: "tool-call" as const,
|
|
111
|
+
toolCallId: "approval-1",
|
|
112
|
+
toolName: "bash",
|
|
113
|
+
args: {},
|
|
114
|
+
argsText: "{}",
|
|
115
|
+
approval: { id: "approval-1" },
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
status: { type: "requires-action" as const, reason: "tool-calls" as const },
|
|
119
|
+
createdAt: new Date(),
|
|
120
|
+
metadata: {
|
|
121
|
+
unstable_state: null,
|
|
122
|
+
unstable_annotations: [],
|
|
123
|
+
unstable_data: [],
|
|
124
|
+
steps: [],
|
|
125
|
+
custom: { toolApprovalThreadId: "main" },
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const decided = applyApprovalDecisionsToMessage(pending, {
|
|
130
|
+
approvalId: "approval-1",
|
|
131
|
+
approved: true,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(messageHasPendingApprovals(decided)).toBe(false);
|
|
135
|
+
expect(collectApprovalInputs(decided, "main")).toHaveLength(1);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MessageStatus,
|
|
3
|
+
ThreadAssistantMessage,
|
|
4
|
+
ThreadAssistantMessagePart,
|
|
5
|
+
ThreadMessage,
|
|
6
|
+
} from "@assistant-ui/core";
|
|
7
|
+
import type {
|
|
8
|
+
ToolApprovalRequiredEvent,
|
|
9
|
+
Turn,
|
|
10
|
+
UserToolApprovalEvent,
|
|
11
|
+
} from "truefoundry-gateway-sdk/agents";
|
|
12
|
+
|
|
13
|
+
import { ROOT_THREAD_ID } from "./constants.js";
|
|
14
|
+
import type { ToolApprovalMessageCustomMetadata } from "./messageCustomMetadata.js";
|
|
15
|
+
import type { TurnStreamUpdate } from "./turnStreamUpdate.js";
|
|
16
|
+
|
|
17
|
+
export { ROOT_THREAD_ID } from "./constants.js";
|
|
18
|
+
|
|
19
|
+
export type StoredApprovalDecision = {
|
|
20
|
+
approved: boolean;
|
|
21
|
+
reason?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const TOOL_APPROVAL_THREAD_ID_CUSTOM_KEY = "toolApprovalThreadId";
|
|
25
|
+
|
|
26
|
+
export type RespondToToolApprovalOptions = {
|
|
27
|
+
approvalId: string;
|
|
28
|
+
approved: boolean;
|
|
29
|
+
optionId?: string;
|
|
30
|
+
reason?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type ApprovalDecision = UserToolApprovalEvent["approval"];
|
|
34
|
+
|
|
35
|
+
type ToolCallPart = Extract<
|
|
36
|
+
ThreadMessage["content"][number],
|
|
37
|
+
{ type: "tool-call" }
|
|
38
|
+
>;
|
|
39
|
+
|
|
40
|
+
type AssistantToolCallPart = Extract<
|
|
41
|
+
ThreadAssistantMessagePart,
|
|
42
|
+
{ type: "tool-call" }
|
|
43
|
+
>;
|
|
44
|
+
|
|
45
|
+
export function hasPendingToolApproval(
|
|
46
|
+
approval: ToolCallPart["approval"] | undefined,
|
|
47
|
+
): boolean {
|
|
48
|
+
return (
|
|
49
|
+
approval != null &&
|
|
50
|
+
approval.approved === undefined &&
|
|
51
|
+
approval.resolution === undefined
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function applyApprovalDecisionToToolCall(
|
|
56
|
+
part: AssistantToolCallPart,
|
|
57
|
+
options: RespondToToolApprovalOptions,
|
|
58
|
+
): AssistantToolCallPart {
|
|
59
|
+
const { approved, optionId, reason } = options;
|
|
60
|
+
const targetApproval = part.approval!;
|
|
61
|
+
const approval = {
|
|
62
|
+
...targetApproval,
|
|
63
|
+
approved,
|
|
64
|
+
...(optionId != null ? { optionId } : {}),
|
|
65
|
+
...(reason != null ? { reason } : {}),
|
|
66
|
+
};
|
|
67
|
+
if (approved) {
|
|
68
|
+
return { ...part, approval };
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
...part,
|
|
72
|
+
approval,
|
|
73
|
+
result: { error: reason || "Tool approval denied" },
|
|
74
|
+
isError: true,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function updateToolApprovalInContent(
|
|
79
|
+
content: readonly ThreadAssistantMessagePart[],
|
|
80
|
+
options: RespondToToolApprovalOptions,
|
|
81
|
+
): { content: readonly ThreadAssistantMessagePart[]; found: boolean } {
|
|
82
|
+
let found = false;
|
|
83
|
+
const newContent = content.map((part) => {
|
|
84
|
+
if (part.type !== "tool-call") {
|
|
85
|
+
return part;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (part.approval?.id === options.approvalId) {
|
|
89
|
+
found = true;
|
|
90
|
+
return applyApprovalDecisionToToolCall(part, options);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (part.messages == null) {
|
|
94
|
+
return part;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const messages = part.messages.map((message) => {
|
|
98
|
+
if (message.role !== "assistant") {
|
|
99
|
+
return message;
|
|
100
|
+
}
|
|
101
|
+
const nested = updateToolApprovalInContent(message.content, options);
|
|
102
|
+
if (!nested.found) {
|
|
103
|
+
return message;
|
|
104
|
+
}
|
|
105
|
+
found = true;
|
|
106
|
+
return { ...message, content: nested.content };
|
|
107
|
+
});
|
|
108
|
+
return { ...part, messages };
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return { content: newContent, found };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function applyApprovalDecisionsToMessage(
|
|
115
|
+
message: ThreadAssistantMessage,
|
|
116
|
+
options: RespondToToolApprovalOptions,
|
|
117
|
+
): ThreadAssistantMessage {
|
|
118
|
+
const { content } = updateToolApprovalInContent(message.content, options);
|
|
119
|
+
return { ...message, content: [...content] };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function toolApprovalStatus(): MessageStatus {
|
|
123
|
+
return { type: "requires-action", reason: "tool-calls" };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function toolApprovalMessageCustom(
|
|
127
|
+
threadId: string,
|
|
128
|
+
): ToolApprovalMessageCustomMetadata {
|
|
129
|
+
return {
|
|
130
|
+
[TOOL_APPROVAL_THREAD_ID_CUSTOM_KEY]:
|
|
131
|
+
threadId === ROOT_THREAD_ID ? ROOT_THREAD_ID : threadId,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function getToolApprovalThreadId(
|
|
136
|
+
message: ThreadMessage | undefined,
|
|
137
|
+
): string | undefined {
|
|
138
|
+
if (message?.role !== "assistant") {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
const threadId = message.metadata.custom[TOOL_APPROVAL_THREAD_ID_CUSTOM_KEY];
|
|
142
|
+
return typeof threadId === "string" ? threadId : undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function findApprovalRequiredInTurn(
|
|
146
|
+
turn: Pick<Turn, "state">,
|
|
147
|
+
): ToolApprovalRequiredEvent | undefined {
|
|
148
|
+
if (turn.state.status !== "done") {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
return turn.state.requiredActions?.find(
|
|
152
|
+
(action): action is ToolApprovalRequiredEvent =>
|
|
153
|
+
action.type === "tool.approval_required",
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function toolCallPartHasPendingApproval(part: ToolCallPart): boolean {
|
|
158
|
+
return hasPendingToolApproval(part.approval);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function nestedMessagesHavePendingApprovals(
|
|
162
|
+
messages: readonly ThreadMessage[],
|
|
163
|
+
): boolean {
|
|
164
|
+
for (const message of messages) {
|
|
165
|
+
if (messageHasPendingApprovals(message)) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function messageHasPendingApprovals(message: ThreadMessage | undefined): boolean {
|
|
173
|
+
if (message?.role !== "assistant") {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
for (const part of message.content) {
|
|
177
|
+
if (part.type !== "tool-call") {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (toolCallPartHasPendingApproval(part)) {
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
if (part.messages != null && nestedMessagesHavePendingApprovals(part.messages)) {
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function walkAssistantToolCallParts(
|
|
191
|
+
content: readonly ThreadAssistantMessagePart[],
|
|
192
|
+
visit: (part: AssistantToolCallPart) => void,
|
|
193
|
+
): void {
|
|
194
|
+
for (const part of content) {
|
|
195
|
+
if (part.type !== "tool-call") {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
visit(part);
|
|
199
|
+
if (part.messages == null) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
for (const message of part.messages) {
|
|
203
|
+
if (message.role === "assistant") {
|
|
204
|
+
walkAssistantToolCallParts(message.content, visit);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function collectDecidedApprovalsFromContent(
|
|
211
|
+
content: readonly ThreadAssistantMessagePart[],
|
|
212
|
+
): Map<string, StoredApprovalDecision> {
|
|
213
|
+
const decisions = new Map<string, StoredApprovalDecision>();
|
|
214
|
+
walkAssistantToolCallParts(content, (part) => {
|
|
215
|
+
const { approval } = part;
|
|
216
|
+
if (approval?.id == null || approval.approved === undefined) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
decisions.set(approval.id, {
|
|
220
|
+
approved: approval.approved,
|
|
221
|
+
...(approval.reason != null ? { reason: approval.reason } : {}),
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
return decisions;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function applyApprovalDecisionsToMessages(
|
|
228
|
+
messages: readonly ThreadMessage[],
|
|
229
|
+
decisions: ReadonlyMap<string, StoredApprovalDecision>,
|
|
230
|
+
): ThreadMessage[] {
|
|
231
|
+
return messages.map((message) => {
|
|
232
|
+
if (message.role !== "assistant") {
|
|
233
|
+
return message;
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
...message,
|
|
237
|
+
content: applyApprovalDecisionsToContent(message.content, decisions),
|
|
238
|
+
};
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function applyApprovalDecisionsToContent(
|
|
243
|
+
content: readonly ThreadAssistantMessagePart[],
|
|
244
|
+
decisions: ReadonlyMap<string, StoredApprovalDecision>,
|
|
245
|
+
): ThreadAssistantMessagePart[] {
|
|
246
|
+
return content.map((part) => {
|
|
247
|
+
if (part.type !== "tool-call") {
|
|
248
|
+
return part;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const decision =
|
|
252
|
+
part.approval?.id != null ? decisions.get(part.approval.id) : undefined;
|
|
253
|
+
let nextPart: AssistantToolCallPart = part;
|
|
254
|
+
|
|
255
|
+
if (
|
|
256
|
+
decision != null &&
|
|
257
|
+
part.approval != null &&
|
|
258
|
+
part.approval.approved === undefined
|
|
259
|
+
) {
|
|
260
|
+
nextPart = applyApprovalDecisionToToolCall(part, {
|
|
261
|
+
approvalId: part.approval.id,
|
|
262
|
+
approved: decision.approved,
|
|
263
|
+
reason: decision.reason,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (nextPart.messages == null) {
|
|
268
|
+
return nextPart;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
...nextPart,
|
|
273
|
+
messages: applyApprovalDecisionsToMessages(nextPart.messages, decisions),
|
|
274
|
+
};
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function mergeDecidedApprovalsIntoContent(
|
|
279
|
+
incoming: readonly ThreadAssistantMessagePart[],
|
|
280
|
+
existing: readonly ThreadAssistantMessagePart[],
|
|
281
|
+
): ThreadAssistantMessagePart[] {
|
|
282
|
+
const decided = collectDecidedApprovalsFromContent(existing);
|
|
283
|
+
if (decided.size === 0) {
|
|
284
|
+
return [...incoming];
|
|
285
|
+
}
|
|
286
|
+
return applyApprovalDecisionsToContent(incoming, decided);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function extractToolApprovalsFromTurnInput(
|
|
290
|
+
input: Turn["input"] | undefined,
|
|
291
|
+
): UserToolApprovalEvent[] {
|
|
292
|
+
const events: UserToolApprovalEvent[] = [];
|
|
293
|
+
for (const item of input ?? []) {
|
|
294
|
+
if (item.type === "user.tool_approval") {
|
|
295
|
+
events.push(item);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return events;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function collectSubsequentApprovalDecisions(
|
|
302
|
+
turns: readonly Pick<Turn, "input">[],
|
|
303
|
+
fromIndex: number,
|
|
304
|
+
): Map<string, StoredApprovalDecision> {
|
|
305
|
+
const decisions = new Map<string, StoredApprovalDecision>();
|
|
306
|
+
|
|
307
|
+
for (let index = fromIndex + 1; index < turns.length; index++) {
|
|
308
|
+
const input = turns[index]?.input ?? [];
|
|
309
|
+
if (input.some((item) => item.type === "user.message")) {
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
for (const event of extractToolApprovalsFromTurnInput(input)) {
|
|
314
|
+
decisions.set(event.toolCallId, {
|
|
315
|
+
approved: event.approval.status === "allow",
|
|
316
|
+
...(event.approval.status === "deny" && event.approval.reason != null
|
|
317
|
+
? { reason: event.approval.reason }
|
|
318
|
+
: {}),
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return decisions;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function collectApprovalDecisionsFromTurnInput(
|
|
327
|
+
input: Turn["input"] | undefined,
|
|
328
|
+
): Map<string, StoredApprovalDecision> {
|
|
329
|
+
const decisions = new Map<string, StoredApprovalDecision>();
|
|
330
|
+
for (const event of extractToolApprovalsFromTurnInput(input)) {
|
|
331
|
+
decisions.set(event.toolCallId, {
|
|
332
|
+
approved: event.approval.status === "allow",
|
|
333
|
+
...(event.approval.status === "deny" && event.approval.reason != null
|
|
334
|
+
? { reason: event.approval.reason }
|
|
335
|
+
: {}),
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
return decisions;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function contentHasPendingApprovals(
|
|
342
|
+
content: readonly ThreadAssistantMessagePart[],
|
|
343
|
+
): boolean {
|
|
344
|
+
return messageHasPendingApprovals({
|
|
345
|
+
id: "pending-check",
|
|
346
|
+
role: "assistant",
|
|
347
|
+
content,
|
|
348
|
+
status: { type: "complete", reason: "stop" },
|
|
349
|
+
createdAt: new Date(),
|
|
350
|
+
metadata: {
|
|
351
|
+
unstable_state: null,
|
|
352
|
+
unstable_annotations: [],
|
|
353
|
+
unstable_data: [],
|
|
354
|
+
steps: [],
|
|
355
|
+
custom: {},
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function resolveToolApprovalUpdate(
|
|
361
|
+
update: TurnStreamUpdate,
|
|
362
|
+
priorDecisions?: ReadonlyMap<string, StoredApprovalDecision>,
|
|
363
|
+
): TurnStreamUpdate {
|
|
364
|
+
let { content } = update;
|
|
365
|
+
if (priorDecisions != null && priorDecisions.size > 0) {
|
|
366
|
+
content = applyApprovalDecisionsToContent(content, priorDecisions);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (
|
|
370
|
+
contentHasPendingApprovals(content) ||
|
|
371
|
+
update.status?.type !== "requires-action" ||
|
|
372
|
+
update.status.reason !== "tool-calls"
|
|
373
|
+
) {
|
|
374
|
+
return { ...update, content };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const { status: _status, metadata: _metadata, ...rest } = update;
|
|
378
|
+
return { ...rest, content };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function mapApprovalDecision(
|
|
382
|
+
approved: boolean,
|
|
383
|
+
reason?: string,
|
|
384
|
+
): ApprovalDecision {
|
|
385
|
+
if (approved) {
|
|
386
|
+
return { status: "allow" };
|
|
387
|
+
}
|
|
388
|
+
return { status: "deny", ...(reason != null ? { reason } : {}) };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function isDecidedApprovalAwaitingSdk(part: ToolCallPart): boolean {
|
|
392
|
+
const { approval, result, isError } = part;
|
|
393
|
+
if (approval?.id == null || approval.approved === undefined) {
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
if (approval.approved === true) {
|
|
397
|
+
return result === undefined;
|
|
398
|
+
}
|
|
399
|
+
return isError === true;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function collectApprovalInputsFromMessages(
|
|
403
|
+
messages: readonly ThreadMessage[],
|
|
404
|
+
defaultThreadId: string,
|
|
405
|
+
): UserToolApprovalEvent[] {
|
|
406
|
+
const events: UserToolApprovalEvent[] = [];
|
|
407
|
+
for (const message of messages) {
|
|
408
|
+
events.push(...collectApprovalInputs(message, defaultThreadId));
|
|
409
|
+
}
|
|
410
|
+
return events;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export function collectApprovalInputs(
|
|
414
|
+
message: ThreadMessage,
|
|
415
|
+
threadId: string,
|
|
416
|
+
): UserToolApprovalEvent[] {
|
|
417
|
+
if (message.role !== "assistant" || !threadId) {
|
|
418
|
+
return [];
|
|
419
|
+
}
|
|
420
|
+
if (messageHasPendingApprovals(message)) {
|
|
421
|
+
return [];
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const scopedThreadId = getToolApprovalThreadId(message) ?? threadId;
|
|
425
|
+
const events: UserToolApprovalEvent[] = [];
|
|
426
|
+
|
|
427
|
+
for (const part of message.content) {
|
|
428
|
+
if (part.type !== "tool-call") {
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
if (isDecidedApprovalAwaitingSdk(part)) {
|
|
432
|
+
const { approval } = part;
|
|
433
|
+
if (approval == null) {
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
events.push({
|
|
437
|
+
type: "user.tool_approval",
|
|
438
|
+
threadId: scopedThreadId,
|
|
439
|
+
toolCallId: approval.id,
|
|
440
|
+
approval: mapApprovalDecision(approval.approved!, approval.reason),
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
if (part.messages != null) {
|
|
444
|
+
events.push(
|
|
445
|
+
...collectApprovalInputsFromMessages(part.messages, scopedThreadId),
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return events;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export function toTrueFoundryApprovalInputs(
|
|
453
|
+
message: Extract<ThreadMessage, { role: "assistant" }>,
|
|
454
|
+
response: RespondToToolApprovalOptions,
|
|
455
|
+
defaultThreadId: string = ROOT_THREAD_ID,
|
|
456
|
+
): UserToolApprovalEvent[] {
|
|
457
|
+
const updated = applyApprovalDecisionsToMessage(message, response);
|
|
458
|
+
return collectApprovalInputs(updated, defaultThreadId);
|
|
459
|
+
}
|