Skip to content

Commit a299042

Browse files
committed
feat: enhance tool approval process with error handling and input blocking
1 parent b43758d commit a299042

8 files changed

Lines changed: 70 additions & 24 deletions

File tree

agent/middleware/apiBasedTools.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ToolMessage } from "@langchain/core/messages";
2+
import { isGraphInterrupt } from "@langchain/langgraph";
23
import { createMiddleware } from "langchain";
34
import { logger, type AdminUser, type IAdminForth } from "adminforth";
45
import {
@@ -13,6 +14,7 @@ import { ALWAYS_AVAILABLE_API_TOOL_NAMES } from "../tools/index.js";
1314
import { createApiTool } from "../tools/apiTool.js";
1415
import type { AgentEventEmitter } from "../../agentEvents.js";
1516
import type { SequenceDebugCollector } from "./sequenceDebug.js";
17+
import { isAbortError } from "../../errors.js";
1618

1719
function getEnabledApiToolNames(messages: unknown[]) {
1820
const enabledToolNames = new Set<string>();
@@ -82,9 +84,14 @@ export function createApiBasedToolsMiddleware(
8284
async wrapToolCall(request, handler) {
8385
const startedAt = Date.now();
8486
const toolInput = JSON.stringify(request.toolCall.args ?? {});
85-
const toolCallId = request.toolCall.id as string;
86-
const { adminUser, emit, sequenceDebugSink, userTimeZone } = request.runtime.context as {
87+
if (!request.toolCall.id) {
88+
throw new Error(`Tool call "${request.toolCall.name}" has no id.`);
89+
}
90+
91+
const toolCallId = request.toolCall.id;
92+
const { adminUser, abortSignal, emit, sequenceDebugSink, userTimeZone } = request.runtime.context as {
8793
adminUser: AdminUser;
94+
abortSignal?: AbortSignal;
8895
emit?: AgentEventEmitter;
8996
sequenceDebugSink: SequenceDebugCollector;
9097
userTimeZone: string;
@@ -137,17 +144,25 @@ export function createApiBasedToolsMiddleware(
137144
toolCallTracker.finishSuccess(result);
138145
return result;
139146
} catch (error) {
147+
if (
148+
isGraphInterrupt(error)
149+
|| abortSignal?.aborted
150+
|| isAbortError(error)
151+
) {
152+
throw error;
153+
}
154+
155+
const message = error instanceof Error ? error.message : String(error);
156+
140157
logger.error(
141158
`Error calling tool "${request.toolCall.name}": ${error instanceof Error ? error.stack ?? error.message : String(error)}`,
142159
);
143-
toolCallTracker.finishError(`Error: ${error instanceof Error ? error.message : String(error)}`);
160+
toolCallTracker.finishError(`Error: ${message}`);
144161
return new ToolMessage({
145162
name: request.toolCall.name,
146163
tool_call_id: toolCallId,
147164
status: "error",
148-
content: `Error: ${
149-
error instanceof Error ? error.message : String(error)
150-
}`,
165+
content: `Error: ${message}`,
151166
})
152167
} finally {
153168
logger.info(

agent/runtime/AgentRuntime.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ export class AgentRuntime {
5151
});
5252
const middleware = [
5353
apiBasedToolsMiddleware,
54+
hitlMiddleware,
5455
...(input.models.modelMiddleware ?? []),
5556
sequenceDebugMiddleware,
56-
hitlMiddleware,
5757
summarizationMiddleware({
5858
model: input.models.summaryModel,
5959
trigger: { tokens: 1024 * 64 },

agentTurnService.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@ export class AgentTurnService {
129129
const approvalDecision = getApprovalDecision(input);
130130
const shouldResume = Boolean(approvalDecision);
131131
const pendingInterrupts = this.pendingInterrupts.get(input.sessionId);
132+
if (shouldResume && (!pendingInterrupts || pendingInterrupts.length === 0)) {
133+
throw new Error(`No pending approval interrupt found for session "${input.sessionId}".`);
134+
}
132135
const lifecycleTurn = shouldResume
133136
? await this.lifecycle.resume(input)
134137
: await this.lifecycle.start(input);

custom/ChatFooter.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
'min-h-12 px-4 pt-4 rounded-xl w-full resize-none overflow-hidden text-lightInputText dark:text-darkInputText rounded-md bg-transparent text-sm bg-gray-50 dark:bg-gray-700 dark:border-gray-600 focus:outline-none',
2121
{ '!text-base': coreStore.isIos }
2222
]"
23-
:placeholder="agentStore.userMessagePlaceholder"
23+
:placeholder="agentStore.hasPendingToolApproval ? 'Approve or reject the pending action to continue' : agentStore.userMessagePlaceholder"
24+
:disabled="agentStore.isMessageInputBlocked"
2425
@keydown.enter.exact.prevent="sendMessage"
2526
/>
2627
<div
@@ -70,7 +71,7 @@
7071
v-if="!agentStore.isResponseInProgress"
7172
class="absolute right-4 bottom-2 !p-0 h-9 w-9 transition-opacity duration-200"
7273
@click="sendMessage"
73-
:disabled="!agentStore.trimmedUserMessage || agentStore.isResponseInProgress"
74+
:disabled="!agentStore.trimmedUserMessage || agentStore.isMessageInputBlocked"
7475
>
7576
<IconArrowUpOutline
7677
class="w-8 h-8 p-1

custom/composables/agentStore/useAgentChat.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,18 @@ export function createAgentChatManager({
8989
replaceLastMessage(assistantMessage);
9090
}
9191

92-
function handleAgentStreamPart(dataPart: any) {
92+
function handleRealtimeChatData(dataPart: any) {
93+
if (dataPart?.type === 'data-open-page' && typeof dataPart.data?.targetPath === 'string') {
94+
onOpenPage(dataPart.data.targetPath);
95+
return;
96+
}
97+
98+
if (dataPart?.type === 'data-interrupt' && typeof dataPart.data?.sessionId === 'string') {
99+
onToolApprovalRequest(dataPart.data.sessionId, dataPart.data.interrupt);
100+
}
101+
}
102+
103+
function handleManualApprovalStreamPart(dataPart: any) {
93104
if (dataPart?.type === 'text-delta' && typeof dataPart.delta === 'string') {
94105
appendTextDelta(dataPart.delta);
95106
return;
@@ -152,7 +163,7 @@ export function createAgentChatManager({
152163
continue;
153164
}
154165

155-
handleAgentStreamPart(JSON.parse(data));
166+
handleManualApprovalStreamPart(JSON.parse(data));
156167
}
157168
}
158169
}
@@ -187,14 +198,7 @@ export function createAgentChatManager({
187198
onError(error: unknown) {
188199
console.error('Chat error:', error);
189200
},
190-
onData(dataPart: any) {
191-
if (dataPart?.type === 'data-open-page' && typeof dataPart.data?.targetPath === 'string') {
192-
onOpenPage(dataPart.data.targetPath);
193-
}
194-
if (dataPart?.type === 'data-interrupt' && typeof dataPart.data?.sessionId === 'string') {
195-
onToolApprovalRequest(dataPart.data.sessionId, dataPart.data.interrupt);
196-
}
197-
},
201+
onData: handleRealtimeChatData,
198202
});
199203
chats.set(sessionId, newChat);
200204
currentChat.value = newChat;

custom/composables/agentStore/useAgentSessions.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type CreateAgentSessionManagerOptions = {
1717
currentChat: ShallowRef<Chat<any> | null | undefined>;
1818
trimmedUserMessage: ComputedRef<string>;
1919
isResponseInProgress: ComputedRef<boolean>;
20+
isMessageInputBlocked: ComputedRef<boolean>;
2021
userMessageInput: Ref<any>;
2122
lastMessage: Ref<string>;
2223
blockCloseOfChat: Ref<boolean>;
@@ -32,6 +33,7 @@ export function createAgentSessionManager({
3233
currentChat,
3334
trimmedUserMessage,
3435
isResponseInProgress,
36+
isMessageInputBlocked,
3537
userMessageInput,
3638
lastMessage,
3739
blockCloseOfChat,
@@ -130,7 +132,7 @@ export function createAgentSessionManager({
130132

131133
async function sendMessage() {
132134
const message = trimmedUserMessage.value;
133-
if (!message || isResponseInProgress.value) {
135+
if (!message || isMessageInputBlocked.value) {
134136
return;
135137
}
136138
if (!currentSession.value || currentSession.value.sessionId === PRE_SESSION_ID) {

custom/composables/useAgentStore.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,10 +152,17 @@ export const useAgentStore = defineStore('agent', () => {
152152
approvalPart.data.status = 'processing';
153153
}
154154

155-
await submitToolApprovalResponse(sessionId, decision);
155+
try {
156+
await submitToolApprovalResponse(sessionId, decision);
156157

157-
if (approvalPart?.data) {
158-
approvalPart.data.status = decision === 'approve' ? 'approved' : 'rejected';
158+
if (approvalPart?.data) {
159+
approvalPart.data.status = decision === 'approve' ? 'approved' : 'rejected';
160+
}
161+
} catch (error) {
162+
if (approvalPart?.data) {
163+
approvalPart.data.status = 'pending';
164+
}
165+
console.error('Error submitting tool approval', error);
159166
}
160167
}
161168

@@ -171,6 +178,18 @@ export const useAgentStore = defineStore('agent', () => {
171178
const isResponseInProgress = computed( () => {
172179
return currentChat.value?.status === 'streaming';
173180
});
181+
const hasPendingToolApproval = computed(() => (
182+
currentChat.value?.messages.some((message: any) => (
183+
message.role === 'assistant'
184+
&& message.parts.some((part: any) => (
185+
part.type === 'data-tool-approval'
186+
&& (part.data?.status === 'pending' || part.data?.status === 'processing')
187+
))
188+
)) ?? false
189+
));
190+
const isMessageInputBlocked = computed(() => (
191+
isResponseInProgress.value || hasPendingToolApproval.value
192+
));
174193
const blockCloseOfChat = ref(false);
175194
const {
176195
sendMessage,
@@ -193,6 +212,7 @@ export const useAgentStore = defineStore('agent', () => {
193212
currentChat,
194213
trimmedUserMessage,
195214
isResponseInProgress,
215+
isMessageInputBlocked,
196216
userMessageInput,
197217
lastMessage,
198218
blockCloseOfChat,
@@ -455,6 +475,8 @@ export const useAgentStore = defineStore('agent', () => {
455475
chatMessages: computed(() => currentChat.value?.messages || []),
456476
trimmedUserMessage,
457477
isResponseInProgress,
478+
hasPendingToolApproval,
479+
isMessageInputBlocked,
458480
isTeleportedToBody,
459481
setIsTeleportedToBody,
460482
chatWidth,

endpoints/core.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@ export function setupCoreEndpoints(ctx: CoreEndpointsContext, server: IHttpServe
9090
await ctx.handleTurn({
9191
prompt: "",
9292
sessionId: data.sessionId,
93-
userTimeZone: 'UTC',
9493
approvalDecision: data.decision,
9594
abortSignal,
9695
adminUser: adminUser!,

0 commit comments

Comments
 (0)