@blocksdiy/react-common 1.51.0 → 1.52.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/dist/components/new-agent-chat.d.ts +11 -0
- package/dist/components/new-agent-chat.d.ts.map +1 -1
- package/dist/components/new-agent-chat.js +202 -80
- package/dist/hooks/agent-chat-own-runs.d.ts +3 -0
- package/dist/hooks/agent-chat-own-runs.d.ts.map +1 -0
- package/dist/hooks/agent-chat-own-runs.js +8 -0
- package/dist/hooks/use-agent-conversation.d.ts +7 -1
- package/dist/hooks/use-agent-conversation.d.ts.map +1 -1
- package/dist/hooks/use-agent-conversation.js +27 -3
- package/dist/utils/agui-interrupts.d.ts +179 -0
- package/dist/utils/agui-interrupts.d.ts.map +1 -0
- package/dist/utils/agui-interrupts.js +351 -0
- package/package.json +2 -2
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { type Message, ToolMessage, type UserMessage } from "@ag-ui/client";
|
|
2
2
|
import { useRenderToolCall } from "@copilotkit/react-core/v2";
|
|
3
3
|
import { ReactNode, SetStateAction } from "react";
|
|
4
|
+
import { type ApprovalCardView, type ApprovalDecision } from "../utils/agui-interrupts.js";
|
|
4
5
|
import { type RenderProp } from "../utils/compose-render.js";
|
|
5
6
|
import { type ComposerMessageMeta } from "../utils/composer-message.js";
|
|
6
7
|
import { type MessageRunIds, type RunStartedAt } from "../utils/message-run-ids.js";
|
|
7
8
|
export { useTextGrowthAnimating } from "../hooks/text-growth-animation.js";
|
|
8
9
|
export { type ComposerMessageMeta, FEEDBACK_MESSAGE_NAME, type FeedbackDraft, type FeedbackRate, feedbackComposerMeta, feedbackDraftOf, feedbackMessage, feedbackThreadId, isFeedbackMessage, isFeedbackTag, parseFeedbackContent, parseFeedbackTag, type ParsedFeedback, } from "../utils/composer-message.js";
|
|
9
10
|
export { MESSAGE_RUN_IDS_EVENT_NAME, messageIdsOfRun, type MessageRunIds, type RunStartedAt, } from "../utils/message-run-ids.js";
|
|
11
|
+
export { type ApprovalCardView, type ApprovalDecision, toolStepVisibleBelowCards } from "../utils/agui-interrupts.js";
|
|
10
12
|
export { type Message, type AssistantMessage, type UserMessage } from "@ag-ui/client";
|
|
11
13
|
export { useRenderTool, useDefaultRenderTool, useHumanInTheLoop, useFrontendTool, ToolCallStatus, } from "@copilotkit/react-core/v2";
|
|
12
14
|
export interface Attachment {
|
|
@@ -78,11 +80,20 @@ export interface AgentChatContextValue {
|
|
|
78
80
|
sendMessage: (message: Omit<UserMessage, "id" | "role">) => Promise<void>;
|
|
79
81
|
sendFromInputs: () => Promise<void>;
|
|
80
82
|
reloadMessages: () => Promise<void>;
|
|
83
|
+
/** @deprecated Answer with `answerInterrupt`; kept for the frozen-copy contract. */
|
|
81
84
|
resolveInterrupt: (resume: unknown) => Promise<boolean>;
|
|
82
85
|
answerInterrupt: (answer: {
|
|
83
86
|
toolCallId: string;
|
|
84
87
|
answer: unknown;
|
|
85
88
|
}) => Promise<void>;
|
|
89
|
+
/** Approve or reject one waiting request; resumes once every request of the pause has an answer. */
|
|
90
|
+
decideApproval?: (decision: {
|
|
91
|
+
interruptId: string;
|
|
92
|
+
index: number;
|
|
93
|
+
decision: ApprovalDecision;
|
|
94
|
+
}) => Promise<void>;
|
|
95
|
+
/** The approval cards on a transcript tool call: waiting, decided here, or recorded. */
|
|
96
|
+
approvalCardsFor?: (toolCallId: string) => ApprovalCardView[];
|
|
86
97
|
stopGeneration: () => void;
|
|
87
98
|
addAttachments: (attachments: Attachment[]) => void;
|
|
88
99
|
removeAttachment: (url: string) => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"new-agent-chat.d.ts","sourceRoot":"","sources":["../../src/components/new-agent-chat.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"new-agent-chat.d.ts","sourceRoot":"","sources":["../../src/components/new-agent-chat.tsx"],"names":[],"mappings":"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"}
|
|
@@ -5,13 +5,16 @@ import { CopilotKit, useCopilotChatInternal } from "@copilotkit/react-core";
|
|
|
5
5
|
import { useCopilotKit, useRenderToolCall } from "@copilotkit/react-core/v2";
|
|
6
6
|
import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
|
|
7
7
|
import { useStickToBottom } from "use-stick-to-bottom";
|
|
8
|
+
import { AgentChatOwnRunsContext } from "../hooks/agent-chat-own-runs.js";
|
|
8
9
|
import { readAgentAdminRoles } from "../utils/agent-admin.js";
|
|
10
|
+
import { answerRouteOf, approvalCardsForToolCall, buildResumeEntries, cancelledResumeEntries, dropStamps, forgetOpenInterrupts, INTERRUPT_NOT_OPEN_ERROR, isApprovalRequest, localStampsByAnchor, mergeStamps, needsReconnectAfter, requestKey, requestsOf, respondMessageOf, singleInterruptResume, stampsAfterInterruptReset, toolCallOf, } from "../utils/agui-interrupts.js";
|
|
9
11
|
import { ComposeRender } from "../utils/compose-render.js";
|
|
10
12
|
import { buildArmedMessageContent, userTextOfContent } from "../utils/composer-message.js";
|
|
11
13
|
import { claimRunInput, EMPTY_MESSAGE_RUN_IDS, EMPTY_RUN_STARTED_AT, findMessageGroupElement, mergeEntries, MESSAGE_IDS_ATTRIBUTE, messageIdsOfRun, readMessageRunIdsEvent, readRunStartedAtEvent, } from "../utils/message-run-ids.js";
|
|
12
14
|
export { useTextGrowthAnimating } from "../hooks/text-growth-animation.js";
|
|
13
15
|
export { FEEDBACK_MESSAGE_NAME, feedbackComposerMeta, feedbackDraftOf, feedbackMessage, feedbackThreadId, isFeedbackMessage, isFeedbackTag, parseFeedbackContent, parseFeedbackTag, } from "../utils/composer-message.js";
|
|
14
16
|
export { MESSAGE_RUN_IDS_EVENT_NAME, messageIdsOfRun, } from "../utils/message-run-ids.js";
|
|
17
|
+
export { toolStepVisibleBelowCards } from "../utils/agui-interrupts.js";
|
|
15
18
|
export { useRenderTool, useDefaultRenderTool, useHumanInTheLoop, useFrontendTool, ToolCallStatus, } from "@copilotkit/react-core/v2";
|
|
16
19
|
// Client-only `useLayoutEffect`, falls back to `useEffect` during SSR.
|
|
17
20
|
// Needed so the `agent.threadId` mirror commits before CopilotKit's
|
|
@@ -53,35 +56,6 @@ const _collectPendingToolCalls = (messages) => {
|
|
|
53
56
|
}
|
|
54
57
|
return pending;
|
|
55
58
|
};
|
|
56
|
-
// Message order IS the order of the interrupt's `action_requests`, so decisions
|
|
57
|
-
// pair with them by index and need no ids. Matched by name rather than by "has no
|
|
58
|
-
// local handler", so a backend tool merely mid-flight is never taken for a pause.
|
|
59
|
-
const _SERVER_PAUSE_TOOL_NAME = "ask_user";
|
|
60
|
-
const _SERVER_PAUSE_TOOL_PREFIX = "ask-component-";
|
|
61
|
-
const _isServerPause = (toolCall) => toolCall.function.name === _SERVER_PAUSE_TOOL_NAME || toolCall.function.name.startsWith(_SERVER_PAUSE_TOOL_PREFIX);
|
|
62
|
-
// ONE assistant message's pauses, never the transcript's: the server batches a
|
|
63
|
-
// turn's pauses into a single interrupt requiring exactly one decision each, and a
|
|
64
|
-
// transcript can also hold pauses from a run that died before parking. Counting
|
|
65
|
-
// those raises the count check — permanently, since the checkpoint never advances.
|
|
66
|
-
const _collectServerPauses = (messages) => {
|
|
67
|
-
const answered = new Set();
|
|
68
|
-
for (const m of messages) {
|
|
69
|
-
if (m.role === "tool" && "toolCallId" in m && typeof m.toolCallId === "string") {
|
|
70
|
-
answered.add(m.toolCallId);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
74
|
-
const m = messages[i];
|
|
75
|
-
if (m.role !== "assistant" || !("toolCalls" in m) || !m.toolCalls) {
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
const pauses = m.toolCalls.filter((toolCall) => !answered.has(toolCall.id) && _isServerPause(toolCall));
|
|
79
|
-
if (pauses.length > 0) {
|
|
80
|
-
return pauses;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return [];
|
|
84
|
-
};
|
|
85
59
|
// CopilotKit 1.60.x: `connectAgent()` awaits every replayed HITL tool call's
|
|
86
60
|
// handler before resolving `isAvailable`, and a `useHumanInTheLoop` handler
|
|
87
61
|
// only resolves on `respond()` — so a transcript replayed with an unanswered
|
|
@@ -91,6 +65,14 @@ const _collectServerPauses = (messages) => {
|
|
|
91
65
|
const isConnectParkedOnPendingToolCall = (messages) => {
|
|
92
66
|
return messages.length > 0 && _collectPendingToolCalls(messages).length > 0;
|
|
93
67
|
};
|
|
68
|
+
const EMPTY_INTERRUPTS = [];
|
|
69
|
+
const EMPTY_ANSWERS = new Map();
|
|
70
|
+
const EMPTY_STAMPS = new Map();
|
|
71
|
+
const EMPTY_REQUESTS = [];
|
|
72
|
+
// What @ag-ui/client tracks as open: the source of truth for what a send must
|
|
73
|
+
// address. The chat writes the field only through `resetInterrupts`, its one writer:
|
|
74
|
+
// on a thread swap and before every connect we start (CopilotKit 1.70.2 never clears it).
|
|
75
|
+
const openInterruptsOf = (agent) => agent?.pendingInterrupts ?? EMPTY_INTERRUPTS;
|
|
94
76
|
const validateAndConvertFiles = (files) => {
|
|
95
77
|
if (files.length === 0) {
|
|
96
78
|
return null;
|
|
@@ -318,6 +300,29 @@ const ChatInitializer = ({ agentBlock, agentChatBlock, chatId, token, appId, com
|
|
|
318
300
|
const [runStartedAt, setRunStartedAt] = useState(EMPTY_RUN_STARTED_AT);
|
|
319
301
|
const liveRunRef = useRef(null);
|
|
320
302
|
const ownRunIdsRef = useRef(new Set());
|
|
303
|
+
// Paired once, when the pause arrives: while the next run streams, a sub-agent's own
|
|
304
|
+
// calls arrive as the parent's and would re-pair the request with them -- a second
|
|
305
|
+
// card on the parent's call.
|
|
306
|
+
const [interruptRequests, setInterruptRequests] = useState(EMPTY_REQUESTS);
|
|
307
|
+
// One answer per request (`requestKey`). The ref is the truth (two clicks in one
|
|
308
|
+
// tick); the state copy re-renders the cards.
|
|
309
|
+
const keyedAnswersRef = useRef(new Map());
|
|
310
|
+
const [interruptAnswers, setInterruptAnswers] = useState(EMPTY_ANSWERS);
|
|
311
|
+
const [localApprovals, setLocalApprovals] = useState(EMPTY_STAMPS);
|
|
312
|
+
// The agent's own `pendingInterrupts` too: CopilotKit 1.70.2 keeps it across a
|
|
313
|
+
// thread swap, and @ag-ui/client 0.0.59 would refuse the next connect.
|
|
314
|
+
// ONE source: @ag-ui/client writes the field on RUN_FINISHED and
|
|
315
|
+
// `onRunFinalized` pairs its requests into `interruptRequests`; this is the only place
|
|
316
|
+
// the chat writes it, and it clears the pairing in the same breath.
|
|
317
|
+
const resetInterrupts = useCallback(({ keepStamps = false } = {}) => {
|
|
318
|
+
forgetOpenInterrupts(copilotAgent);
|
|
319
|
+
setInterruptRequests(EMPTY_REQUESTS);
|
|
320
|
+
keyedAnswersRef.current.clear();
|
|
321
|
+
setInterruptAnswers(EMPTY_ANSWERS);
|
|
322
|
+
setLocalApprovals((previous) => stampsAfterInterruptReset(previous, { keepStamps }));
|
|
323
|
+
}, [copilotAgent]);
|
|
324
|
+
// Runs this chat started that no send accounted for (AgentChatOwnRunsContext).
|
|
325
|
+
const [ownRunsWithoutSend, setOwnRunsWithoutSend] = useState(0);
|
|
321
326
|
useEffect(() => {
|
|
322
327
|
if (!copilotAgent) {
|
|
323
328
|
return;
|
|
@@ -349,6 +354,9 @@ const ChatInitializer = ({ agentBlock, agentChatBlock, chatId, token, appId, com
|
|
|
349
354
|
onToolCallStartEvent: ({ event }) => claimLive(event.parentMessageId ?? event.toolCallId),
|
|
350
355
|
onRunFinalized: () => {
|
|
351
356
|
liveRunRef.current = null;
|
|
357
|
+
// The agent's own field, which @ag-ui/client has set on RUN_FINISHED by now (pinned
|
|
358
|
+
// in agui-interrupts.spec) -- live runs and connects alike; a RUN_ERROR leaves it.
|
|
359
|
+
setInterruptRequests(requestsOf(openInterruptsOf(copilotAgent), copilotAgent.messages));
|
|
352
360
|
},
|
|
353
361
|
onCustomEvent: ({ event }) => {
|
|
354
362
|
const runIds = readMessageRunIdsEvent(event);
|
|
@@ -366,7 +374,6 @@ const ChatInitializer = ({ agentBlock, agentChatBlock, chatId, token, appId, com
|
|
|
366
374
|
liveRunRef.current = null;
|
|
367
375
|
};
|
|
368
376
|
}, [copilotAgent]);
|
|
369
|
-
const interruptAnswersRef = useRef(new Map());
|
|
370
377
|
// Mirror `currentThreadId` onto the agent instance — the `<CopilotKit>`
|
|
371
378
|
// prop only feeds React context, but `agent.threadId` is what hits the
|
|
372
379
|
// wire and the LangGraph checkpoint. `<CopilotChat>` does this for you;
|
|
@@ -380,8 +387,8 @@ const ChatInitializer = ({ agentBlock, agentChatBlock, chatId, token, appId, com
|
|
|
380
387
|
}
|
|
381
388
|
// Carrying answers across a thread swap would pair them with another
|
|
382
389
|
// conversation's pause.
|
|
383
|
-
|
|
384
|
-
}, [copilotAgent, currentThreadId]);
|
|
390
|
+
resetInterrupts();
|
|
391
|
+
}, [copilotAgent, currentThreadId, resetInterrupts]);
|
|
385
392
|
const [prompt, setPrompt] = useState("");
|
|
386
393
|
const [attachments, setAttachments] = useState([]);
|
|
387
394
|
const [composerMessageMeta, setComposerMessageMeta] = useState(null);
|
|
@@ -404,6 +411,7 @@ const ChatInitializer = ({ agentBlock, agentChatBlock, chatId, token, appId, com
|
|
|
404
411
|
setPrompt("");
|
|
405
412
|
setAttachments([]);
|
|
406
413
|
setComposerMessageMeta(null);
|
|
414
|
+
resetInterrupts();
|
|
407
415
|
setOwnRunInFlight(false);
|
|
408
416
|
const detachable = copilotAgent;
|
|
409
417
|
const drained = detachable?.detachActiveRun?.();
|
|
@@ -427,18 +435,25 @@ const ChatInitializer = ({ agentBlock, agentChatBlock, chatId, token, appId, com
|
|
|
427
435
|
}
|
|
428
436
|
copilotAgent.setState?.({});
|
|
429
437
|
copilotAgent.clearReplayCursor?.(currentThreadId);
|
|
438
|
+
// Answers and decided-here stamps too; the connect re-sends what is open.
|
|
439
|
+
resetInterrupts();
|
|
430
440
|
void copilotkit.connectAgent({ agent: copilotAgent }).catch(() => undefined);
|
|
431
|
-
}, [chatId, currentThreadId, copilotAgent, copilotkit]);
|
|
441
|
+
}, [chatId, currentThreadId, copilotAgent, copilotkit, resetInterrupts]);
|
|
432
442
|
const [isDraggingFiles, setIsDraggingFiles] = useState(false);
|
|
433
443
|
const attachmentsEnabled = agentChat ? agentChat.disableAttachments !== true : false;
|
|
434
444
|
const canSendMessage = Boolean(agentChatBlock && agentChat && agent && isConfigLoaded && isAgentReady && !isAgentRunning && !isRunningElsewhere);
|
|
435
445
|
const isFetchingMessages = !isConfigLoaded || (!isAgentReady && copilotMessages.length === 0);
|
|
436
446
|
const isThinking = (isAgentRunning || isRunningElsewhere) && !isFetchingMessages && isAgentReady;
|
|
437
|
-
const runAgentWithRetry = useCallback(async (agentToRun, messageCountAtRunStart, forwardedProps) => {
|
|
447
|
+
const runAgentWithRetry = useCallback(async (agentToRun, messageCountAtRunStart, forwardedProps, resume, withoutSend = false) => {
|
|
438
448
|
// A run elsewhere is on this conversation's checkpoint too: ours would race it.
|
|
439
449
|
if (agentToRun.isRunning || isRunningElsewhereRef.current) {
|
|
440
450
|
return false;
|
|
441
451
|
}
|
|
452
|
+
// A turn on the server like any run; a send accounts for its own (onUserMessageSent).
|
|
453
|
+
if (withoutSend) {
|
|
454
|
+
setOwnRunsWithoutSend((count) => count + 1);
|
|
455
|
+
}
|
|
456
|
+
let finished = false;
|
|
442
457
|
const runAgentAndThrowError = async () => {
|
|
443
458
|
let runEventError;
|
|
444
459
|
const { unsubscribe } = agentToRun.subscribe({
|
|
@@ -448,9 +463,12 @@ const ChatInitializer = ({ agentBlock, agentChatBlock, chatId, token, appId, com
|
|
|
448
463
|
onRunErrorEvent: (event) => {
|
|
449
464
|
runEventError = event;
|
|
450
465
|
},
|
|
466
|
+
onRunFinishedEvent: () => {
|
|
467
|
+
finished = true;
|
|
468
|
+
},
|
|
451
469
|
});
|
|
452
470
|
try {
|
|
453
|
-
await copilotkit.runAgent({ agent: agentToRun, forwardedProps });
|
|
471
|
+
await copilotkit.runAgent({ agent: agentToRun, forwardedProps, ...(resume ? { resume } : {}) });
|
|
454
472
|
if (runEventError) {
|
|
455
473
|
throw runEventError;
|
|
456
474
|
}
|
|
@@ -462,26 +480,41 @@ const ChatInitializer = ({ agentBlock, agentChatBlock, chatId, token, appId, com
|
|
|
462
480
|
// In flight until the run ends, whichever way (see `runningElsewhereRunId`) — unless
|
|
463
481
|
// the conversation switched under it, which resets this itself.
|
|
464
482
|
const ranOn = prevConversationKeyRef.current;
|
|
483
|
+
let reconnected = false;
|
|
465
484
|
setOwnRunInFlight(true);
|
|
466
485
|
try {
|
|
467
486
|
await runAgentAndThrowError();
|
|
468
|
-
return true;
|
|
469
487
|
}
|
|
470
488
|
catch (error) {
|
|
471
489
|
const hasAgentStartedResponding = agentToRun.messages.length > messageCountAtRunStart;
|
|
472
490
|
const status = getAgentRunErrorStatus(error);
|
|
473
|
-
|
|
491
|
+
// Never re-sent blind when it carried a resume: the server may already have consumed
|
|
492
|
+
// it. The reconnect below re-reads what is open instead.
|
|
493
|
+
if (!resume && status !== undefined && status >= 500 && status < 600 && !hasAgentStartedResponding) {
|
|
474
494
|
await runAgentAndThrowError();
|
|
475
|
-
return true;
|
|
476
495
|
}
|
|
477
|
-
|
|
496
|
+
else {
|
|
497
|
+
throw error;
|
|
498
|
+
}
|
|
478
499
|
}
|
|
479
500
|
finally {
|
|
480
|
-
|
|
501
|
+
const stillHere = prevConversationKeyRef.current === ranOn;
|
|
502
|
+
if (stillHere) {
|
|
481
503
|
setOwnRunInFlight(false);
|
|
482
504
|
}
|
|
505
|
+
if (stillHere && needsReconnectAfter(Boolean(resume), finished)) {
|
|
506
|
+
// A RUN_ERROR, a stop or a dropped stream leaves @ag-ui/client's
|
|
507
|
+
// `pendingInterrupts` as they were, possibly consumed: every later send would be
|
|
508
|
+
// refused. Forget them and reconnect; the connect re-sends what is really open.
|
|
509
|
+
resetInterrupts();
|
|
510
|
+
agentToRun.clearReplayCursor?.(currentThreadId);
|
|
511
|
+
await copilotkit.connectAgent({ agent: agentToRun }).catch(() => undefined);
|
|
512
|
+
reconnected = true;
|
|
513
|
+
}
|
|
483
514
|
}
|
|
484
|
-
|
|
515
|
+
// A reconnected run did not take effect as sent: callers must not record it as done.
|
|
516
|
+
return !reconnected;
|
|
517
|
+
}, [copilotkit, currentThreadId, resetInterrupts]);
|
|
485
518
|
const sendMessage = useCallback(async (message) => {
|
|
486
519
|
if (!canSendMessage || !copilotAgent || copilotAgent.isRunning || isRunningElsewhereRef.current) {
|
|
487
520
|
return;
|
|
@@ -490,71 +523,157 @@ const ChatInitializer = ({ agentBlock, agentChatBlock, chatId, token, appId, com
|
|
|
490
523
|
if (onUserMessageSent) {
|
|
491
524
|
onUserMessageSent(userTextOfContent(message.content), message);
|
|
492
525
|
}
|
|
493
|
-
|
|
526
|
+
// Typing while the turn is parked sends the message
|
|
527
|
+
// (already added above, with its own id and attachments) as a normal message,
|
|
528
|
+
// plus a payload-less `cancelled` entry for every open interrupt --
|
|
529
|
+
// @ag-ui/client refuses to send at all unless every open interrupt is
|
|
530
|
+
// addressed. The server rejects every waiting approval (one approved here but
|
|
531
|
+
// not yet sent included), skips every question with one fixed marker, and
|
|
532
|
+
// keeps the message after them. A send that fails reconnects (runAgentWithRetry).
|
|
533
|
+
const interrupts = openInterruptsOf(copilotAgent);
|
|
534
|
+
if (interrupts.length === 0) {
|
|
535
|
+
await runAgentWithRetry(copilotAgent, copilotAgent.messages.length);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const requests = requestsOf(interrupts, copilotAgent.messages);
|
|
539
|
+
const rejected = new Map(requests.filter(isApprovalRequest).map((request) => [request.key, { type: "reject" }]));
|
|
540
|
+
// Shown as decided for the whole run, as a click is (see submitInterruptAnswers):
|
|
541
|
+
// approvals rejected, questions closed too (they get the skip marker), so no card
|
|
542
|
+
// reads "waiting for your decision on the other request". The ref stays empty so
|
|
543
|
+
// no card can send.
|
|
544
|
+
keyedAnswersRef.current.clear();
|
|
545
|
+
setInterruptAnswers(new Map(requests.map((request) => [request.key, rejected.get(request.key) ?? { type: "respond", message: null }])));
|
|
546
|
+
const decided = localStampsByAnchor(requests, rejected);
|
|
547
|
+
setLocalApprovals((previous) => mergeStamps(previous, decided));
|
|
548
|
+
let resumed = false;
|
|
549
|
+
try {
|
|
550
|
+
resumed = await runAgentWithRetry(copilotAgent, copilotAgent.messages.length, undefined, cancelledResumeEntries(interrupts));
|
|
551
|
+
}
|
|
552
|
+
finally {
|
|
553
|
+
if (!resumed) {
|
|
554
|
+
setLocalApprovals((previous) => dropStamps(previous, decided));
|
|
555
|
+
}
|
|
556
|
+
setInterruptAnswers(EMPTY_ANSWERS);
|
|
557
|
+
}
|
|
494
558
|
}, [canSendMessage, copilotAgent, runAgentWithRetry, onUserMessageSent]);
|
|
495
559
|
const reloadMessages = useCallback(async () => {
|
|
496
560
|
if (!copilotAgent || copilotAgent.isRunning) {
|
|
497
561
|
return;
|
|
498
562
|
}
|
|
499
563
|
copilotAgent.clearReplayCursor?.(currentThreadId);
|
|
564
|
+
// Answers too; the connect re-sends what is open. The decided-here stamps stay: on the
|
|
565
|
+
// same thread they bridge the card until the connect's snapshot brings the record.
|
|
566
|
+
resetInterrupts({ keepStamps: true });
|
|
500
567
|
await copilotkit.connectAgent({ agent: copilotAgent }).catch(() => undefined);
|
|
501
|
-
}, [copilotAgent, copilotkit, currentThreadId]);
|
|
502
|
-
//
|
|
503
|
-
//
|
|
504
|
-
//
|
|
505
|
-
// double-click a no-op rather than a duplicate resume.
|
|
568
|
+
}, [copilotAgent, copilotkit, currentThreadId, resetInterrupts]);
|
|
569
|
+
// Deprecated: no renderer calls it, and ChatComponentPreview stubs
|
|
570
|
+
// it. The context field stays for the frozen-copy contract; a `{decisions}` payload now
|
|
571
|
+
// goes to the ONE open interrupt through the standard `resume[]`.
|
|
506
572
|
const resolveInterrupt = useCallback(async (resume) => {
|
|
507
573
|
if (!copilotAgent) {
|
|
508
574
|
return false;
|
|
509
575
|
}
|
|
510
|
-
|
|
576
|
+
const entries = singleInterruptResume(openInterruptsOf(copilotAgent), resume);
|
|
577
|
+
if (!entries) {
|
|
578
|
+
throw new Error(INTERRUPT_NOT_OPEN_ERROR);
|
|
579
|
+
}
|
|
580
|
+
return runAgentWithRetry(copilotAgent, copilotAgent.messages.length, undefined, entries, true);
|
|
511
581
|
}, [copilotAgent, runAgentWithRetry]);
|
|
512
|
-
// Resumes
|
|
513
|
-
//
|
|
514
|
-
|
|
515
|
-
const answerInterrupt = useCallback(async ({ toolCallId, answer }) => {
|
|
582
|
+
// Resumes once EVERY request of every open interrupt has an answer: the server
|
|
583
|
+
// refuses a partial resume.
|
|
584
|
+
const submitInterruptAnswers = useCallback(async () => {
|
|
516
585
|
if (!copilotAgent) {
|
|
517
586
|
return;
|
|
518
587
|
}
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
}
|
|
525
|
-
const answers = interruptAnswersRef.current;
|
|
526
|
-
answers.set(toolCallId, answer);
|
|
527
|
-
const unanswered = pauses.filter((toolCall) => !answers.has(toolCall.id));
|
|
528
|
-
if (unanswered.length > 0) {
|
|
588
|
+
const interrupts = openInterruptsOf(copilotAgent);
|
|
589
|
+
const requests = requestsOf(interrupts, copilotAgent.messages);
|
|
590
|
+
const answers = keyedAnswersRef.current;
|
|
591
|
+
const resume = buildResumeEntries(interrupts, requests, answers);
|
|
592
|
+
if (!resume) {
|
|
529
593
|
return;
|
|
530
594
|
}
|
|
531
|
-
const decisions = pauses.map((toolCall) => ({
|
|
532
|
-
type: "respond",
|
|
533
|
-
// `?? null` because a component may call `respond()` with no payload, and
|
|
534
|
-
// `JSON.stringify(undefined)` drops the key, which raises server-side.
|
|
535
|
-
message: JSON.stringify(answers.get(toolCall.id) ?? null),
|
|
536
|
-
}));
|
|
537
595
|
const sent = new Map(answers);
|
|
538
596
|
answers.clear();
|
|
539
|
-
|
|
597
|
+
// Stamped before the run: once the run streams, the call stops waiting and the live
|
|
598
|
+
// request no longer pairs with it, so without the stamp the card vanishes until the
|
|
599
|
+
// run ends and then comes back lower.
|
|
600
|
+
const decided = localStampsByAnchor(requests, sent);
|
|
601
|
+
setLocalApprovals((previous) => mergeStamps(previous, decided));
|
|
540
602
|
let resumed = false;
|
|
541
603
|
try {
|
|
542
|
-
|
|
543
|
-
// happened discards the rest of the turn's cards with it.
|
|
544
|
-
resumed = await resolveInterrupt({ decisions });
|
|
604
|
+
resumed = await runAgentWithRetry(copilotAgent, copilotAgent.messages.length, undefined, resume, true);
|
|
545
605
|
}
|
|
546
606
|
catch (error) {
|
|
547
|
-
|
|
607
|
+
// Sent and failed (a refused resume, a server error): runAgentWithRetry already
|
|
608
|
+
// reconnected, so the cards show what is really open.
|
|
609
|
+
// Restoring the answers would show "Approved" for a decision that never took
|
|
610
|
+
// effect.
|
|
611
|
+
setLocalApprovals((previous) => dropStamps(previous, decided));
|
|
612
|
+
setInterruptAnswers(EMPTY_ANSWERS);
|
|
548
613
|
throw error;
|
|
549
614
|
}
|
|
550
615
|
if (!resumed) {
|
|
551
|
-
//
|
|
552
|
-
//
|
|
553
|
-
//
|
|
554
|
-
|
|
555
|
-
|
|
616
|
+
// Nothing was sent (a run already in flight), or the run did not finish and the
|
|
617
|
+
// chat reloaded what is open. Clear the answers, as on an error: restoring them
|
|
618
|
+
// would lock every card on "Approved"/"Rejected" with no way to resend until the
|
|
619
|
+
// user typed or reloaded. The cards go back to waiting, and the next click sends.
|
|
620
|
+
setLocalApprovals((previous) => dropStamps(previous, decided));
|
|
621
|
+
setInterruptAnswers(EMPTY_ANSWERS);
|
|
622
|
+
throw new Error("answerInterrupt: the answer did not go through; the chat shows what is still open");
|
|
623
|
+
}
|
|
624
|
+
setInterruptAnswers(EMPTY_ANSWERS);
|
|
625
|
+
}, [copilotAgent, runAgentWithRetry]);
|
|
626
|
+
const decideApproval = useCallback(async ({ interruptId, index, decision }) => {
|
|
627
|
+
keyedAnswersRef.current.set(requestKey(interruptId, index), { type: decision });
|
|
628
|
+
setInterruptAnswers(new Map(keyedAnswersRef.current));
|
|
629
|
+
await submitInterruptAnswers();
|
|
630
|
+
}, [submitInterruptAnswers]);
|
|
631
|
+
const approvalCardsFor = useCallback((toolCallId) => {
|
|
632
|
+
const call = toolCallOf(copilotMessages, toolCallId);
|
|
633
|
+
const toolMessage = toolMessages.find((message) => message.toolCallId === toolCallId);
|
|
634
|
+
return approvalCardsForToolCall({
|
|
635
|
+
toolCallId,
|
|
636
|
+
toolName: call?.name ?? "",
|
|
637
|
+
toolArgs: call?.args,
|
|
638
|
+
toolResult: typeof toolMessage?.content === "string" ? toolMessage.content : null,
|
|
639
|
+
toolMessage,
|
|
640
|
+
requests: interruptRequests,
|
|
641
|
+
answers: interruptAnswers,
|
|
642
|
+
localStamps: localApprovals,
|
|
643
|
+
});
|
|
644
|
+
}, [copilotMessages, toolMessages, interruptRequests, interruptAnswers, localApprovals]);
|
|
645
|
+
// One path for every question card. Every renderer -- the frozen registry copies
|
|
646
|
+
// included -- calls this with the same signature. The answer goes to the open request its
|
|
647
|
+
// card pairs with (by order) and resumes through `resume[]` once every request has one.
|
|
648
|
+
// The welcome seed, which has no interrupt,
|
|
649
|
+
// becomes its call's tool result and a normal run. Anything else throws, never falls
|
|
650
|
+
// back: the card resets and stays waiting, and typing still works.
|
|
651
|
+
const answerInterrupt = useCallback(async ({ toolCallId, answer }) => {
|
|
652
|
+
if (!copilotAgent) {
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
const route = answerRouteOf(openInterruptsOf(copilotAgent), copilotAgent.messages, toolCallId);
|
|
656
|
+
if (route.kind === "respond") {
|
|
657
|
+
keyedAnswersRef.current.set(route.request.key, { type: "respond", message: answer });
|
|
658
|
+
setInterruptAnswers(new Map(keyedAnswersRef.current));
|
|
659
|
+
await submitInterruptAnswers();
|
|
660
|
+
return;
|
|
556
661
|
}
|
|
557
|
-
|
|
662
|
+
if (route.kind === "refuse") {
|
|
663
|
+
throw new Error(INTERRUPT_NOT_OPEN_ERROR);
|
|
664
|
+
}
|
|
665
|
+
// The welcome seed: the call's tool result, then an ordinary run --
|
|
666
|
+
// no resume, nothing parked on the server. A run that fails keeps the answer, like
|
|
667
|
+
// a typed message whose run failed.
|
|
668
|
+
const seedAnswerId = generateId();
|
|
669
|
+
copilotAgent.addMessage({ id: seedAnswerId, role: "tool", toolCallId, content: respondMessageOf(answer) });
|
|
670
|
+
const sent = await runAgentWithRetry(copilotAgent, copilotAgent.messages.length, undefined, undefined, true);
|
|
671
|
+
if (!sent) {
|
|
672
|
+
// Nothing went out (a run already in flight): take the answer back so the card can send again.
|
|
673
|
+
copilotAgent.setMessages?.(copilotAgent.messages.filter((message) => message.id !== seedAnswerId));
|
|
674
|
+
throw new Error("answerInterrupt: a run was already in flight; answer not sent");
|
|
675
|
+
}
|
|
676
|
+
}, [copilotAgent, runAgentWithRetry, submitInterruptAnswers]);
|
|
558
677
|
const stopGeneration = useCallback(() => {
|
|
559
678
|
copilotStopGeneration();
|
|
560
679
|
copilotAgent?.abortRun?.();
|
|
@@ -825,6 +944,8 @@ const ChatInitializer = ({ agentBlock, agentChatBlock, chatId, token, appId, com
|
|
|
825
944
|
reloadMessages,
|
|
826
945
|
resolveInterrupt,
|
|
827
946
|
answerInterrupt,
|
|
947
|
+
decideApproval,
|
|
948
|
+
approvalCardsFor,
|
|
828
949
|
stopGeneration,
|
|
829
950
|
addAttachments,
|
|
830
951
|
removeAttachment,
|
|
@@ -845,7 +966,7 @@ const ChatInitializer = ({ agentBlock, agentChatBlock, chatId, token, appId, com
|
|
|
845
966
|
components: components ?? [],
|
|
846
967
|
componentsLoading,
|
|
847
968
|
currentThreadId,
|
|
848
|
-
}, children: children }));
|
|
969
|
+
}, children: _jsx(AgentChatOwnRunsContext.Provider, { value: ownRunsWithoutSend, children: children }) }));
|
|
849
970
|
};
|
|
850
971
|
export const AgentChatRoot = ({ ...props
|
|
851
972
|
// agentChatData,
|
|
@@ -1007,6 +1128,7 @@ export const AgentChatRoot = ({ ...props
|
|
|
1007
1128
|
noPersistency,
|
|
1008
1129
|
chatContext: chatContextStr,
|
|
1009
1130
|
chatContextFiles,
|
|
1131
|
+
supportsInterruptOutcome: true,
|
|
1010
1132
|
...(selfEditEnabled ? { selfEditEnabled: true } : {}),
|
|
1011
1133
|
...(supportsServerInterrupt ? { supportsServerInterrupt: true } : {}),
|
|
1012
1134
|
...(serverInterruptComponentIds.length ? { serverInterruptComponentIds } : {}),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-chat-own-runs.d.ts","sourceRoot":"","sources":["../../src/hooks/agent-chat-own-runs.ts"],"names":[],"mappings":"AAQA,mFAAmF;AACnF,eAAO,MAAM,uBAAuB,wCAAqC,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// The chat root → conversation bridge for runs no send accounts for. The server counts a turn
|
|
2
|
+
// per run, the chat's send seam (onUserMessageSent) one per send; a resume (an approval, an
|
|
3
|
+
// answered question) is a run with no send. The chat root provides how many such runs it has
|
|
4
|
+
// started; useAgentConversation accounts each as this client's own turn. Its own module so
|
|
5
|
+
// the threads entry does not pull in the chat runtime.
|
|
6
|
+
import { createContext } from "react";
|
|
7
|
+
/** Runs this chat started that no send accounted for; null outside a chat root. */
|
|
8
|
+
export const AgentChatOwnRunsContext = createContext(null);
|
|
@@ -67,6 +67,11 @@ export declare const accountedTurnsAfterRow: (accounted: AccountedTurns | null,
|
|
|
67
67
|
* starts the count, so the row that lands for it is read as this send's, not a foreign turn.
|
|
68
68
|
*/
|
|
69
69
|
export declare const accountedTurnsAfterOwnSend: (accounted: AccountedTurns | null, key: string, isDraft: boolean) => AccountedTurns | null;
|
|
70
|
+
/**
|
|
71
|
+
* The accounting after this client starts `runs` runs on `key` that no send accounted for (a
|
|
72
|
+
* resume: an approval, an answered question). Each is a turn on the server, as a send is.
|
|
73
|
+
*/
|
|
74
|
+
export declare const accountedTurnsAfterOwnRuns: (accounted: AccountedTurns | null, key: string, isDraft: boolean, runs: number) => AccountedTurns | null;
|
|
70
75
|
/** The chat runtime's side of the bridge — a subset of react-common's AgentChatContextValue. */
|
|
71
76
|
export interface AgentConversationChat {
|
|
72
77
|
isThinking: boolean;
|
|
@@ -97,7 +102,8 @@ export interface UseAgentConversationResult {
|
|
|
97
102
|
/**
|
|
98
103
|
* Keeps the index and the transcript agreeing for the open conversation:
|
|
99
104
|
* - a run on it this client is not streaming (the listed row's `runningRunId`) reads as the chat's own;
|
|
100
|
-
* - a turn this client did not
|
|
105
|
+
* - a turn this client did not make (`turnCount` ahead of what it accounted for: its sends, and
|
|
106
|
+
* the runs the chat root started without one) replays the transcript;
|
|
101
107
|
* - a run mark past the stale ceiling asks the server again (the settle publishes no event);
|
|
102
108
|
* - refocus re-walks the list (realtime has no replay), throttled.
|
|
103
109
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-agent-conversation.d.ts","sourceRoot":"","sources":["../../src/hooks/use-agent-conversation.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"use-agent-conversation.d.ts","sourceRoot":"","sources":["../../src/hooks/use-agent-conversation.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
// (useAgentThreads) and the chat runtime (useAgentChat): which conversation is selected and
|
|
3
3
|
// how a draft becomes a thread; the conversation's row; and the bridge that keeps the index
|
|
4
4
|
// and the transcript agreeing. UI-free — the agent-layout registry item renders it.
|
|
5
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
5
|
+
import { useCallback, useContext, useEffect, useRef, useState } from "react";
|
|
6
6
|
import { FEEDBACK_MESSAGE_NAME } from "../utils/composer-message.js";
|
|
7
|
+
import { AgentChatOwnRunsContext } from "./agent-chat-own-runs.js";
|
|
7
8
|
import { agentThreadKey } from "./agent-thread-live-update.js";
|
|
8
9
|
export const newDraftSelection = (chatId, draftThreadId = crypto.randomUUID()) => ({
|
|
9
10
|
conversation: { chatId, chatThreadId: draftThreadId },
|
|
@@ -106,10 +107,22 @@ export const accountedTurnsAfterOwnSend = (accounted, key, isDraft) => {
|
|
|
106
107
|
}
|
|
107
108
|
return isDraft ? { key, turns: 1 } : accounted;
|
|
108
109
|
};
|
|
110
|
+
/**
|
|
111
|
+
* The accounting after this client starts `runs` runs on `key` that no send accounted for (a
|
|
112
|
+
* resume: an approval, an answered question). Each is a turn on the server, as a send is.
|
|
113
|
+
*/
|
|
114
|
+
export const accountedTurnsAfterOwnRuns = (accounted, key, isDraft, runs) => {
|
|
115
|
+
let next = accounted;
|
|
116
|
+
for (let run = 0; run < runs; run++) {
|
|
117
|
+
next = accountedTurnsAfterOwnSend(next, key, isDraft);
|
|
118
|
+
}
|
|
119
|
+
return next;
|
|
120
|
+
};
|
|
109
121
|
/**
|
|
110
122
|
* Keeps the index and the transcript agreeing for the open conversation:
|
|
111
123
|
* - a run on it this client is not streaming (the listed row's `runningRunId`) reads as the chat's own;
|
|
112
|
-
* - a turn this client did not
|
|
124
|
+
* - a turn this client did not make (`turnCount` ahead of what it accounted for: its sends, and
|
|
125
|
+
* the runs the chat root started without one) replays the transcript;
|
|
113
126
|
* - a run mark past the stale ceiling asks the server again (the settle publishes no event);
|
|
114
127
|
* - refocus re-walks the list (realtime has no replay), throttled.
|
|
115
128
|
*/
|
|
@@ -177,9 +190,20 @@ export const useAgentConversation = ({ threads, agentId, conversation, isDraft,
|
|
|
177
190
|
window.addEventListener("focus", onFocus);
|
|
178
191
|
return () => window.removeEventListener("focus", onFocus);
|
|
179
192
|
}, [refresh]);
|
|
180
|
-
// Turns this client did not
|
|
193
|
+
// Turns this client did not make (see accountedTurnsAfterRow). `isThinking` is a dependency,
|
|
181
194
|
// so this runs again the moment a run on the conversation settles.
|
|
182
195
|
const accountedTurnsRef = useRef(null);
|
|
196
|
+
// The chat root's runs without a send, accounted before the row effect below reads a settle.
|
|
197
|
+
const ownRunsWithoutSend = useContext(AgentChatOwnRunsContext);
|
|
198
|
+
const seenOwnRunsRef = useRef(ownRunsWithoutSend);
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
const seen = seenOwnRunsRef.current;
|
|
201
|
+
seenOwnRunsRef.current = ownRunsWithoutSend;
|
|
202
|
+
if (seen === null || ownRunsWithoutSend === null || ownRunsWithoutSend <= seen) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
accountedTurnsRef.current = accountedTurnsAfterOwnRuns(accountedTurnsRef.current, conversationKey, isDraft, ownRunsWithoutSend - seen);
|
|
206
|
+
}, [ownRunsWithoutSend, conversationKey, isDraft]);
|
|
183
207
|
useEffect(() => {
|
|
184
208
|
if (!knownRow) {
|
|
185
209
|
return;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a parked agent turn waits on, and how the chat answers it.
|
|
3
|
+
*
|
|
4
|
+
* The server ends a parked run with the AG-UI standard `RUN_FINISHED.outcome`
|
|
5
|
+
* (`{type: "interrupt", interrupts}`), live and on connect. Each interrupt carries
|
|
6
|
+
* HumanInTheLoopMiddleware's requests in `metadata.langgraph.raw` -- name, args and
|
|
7
|
+
* description, no ids. One model step batches ask_user and approvals into ONE
|
|
8
|
+
* interrupt, and two paused sub-agents are two interrupts, so the chat collects one
|
|
9
|
+
* answer per request and resumes once with `resume[]`.
|
|
10
|
+
*
|
|
11
|
+
* Cards pair with tool calls BY ORDER, as ask_user cards already do:
|
|
12
|
+
* the middleware builds its requests in the waiting message's tool-call order, so
|
|
13
|
+
* the i-th request of a name is the i-th unanswered call of that name. A request
|
|
14
|
+
* with no such call belongs to a sub-agent, whose calls the parent transcript does
|
|
15
|
+
* not show: it hangs on the waiting `task` call. The server pairs the same way
|
|
16
|
+
* (wf-actions-py `hitl_pairing`). A wrong match can only misplace a card: the card
|
|
17
|
+
* draws the request's own name and args, and decisions stay positional.
|
|
18
|
+
*
|
|
19
|
+
* A past decision comes back on the tool message as `metadata["blocks.toolApprovals"]`.
|
|
20
|
+
* Who decided and when is a server-side audit record and is never shown.
|
|
21
|
+
*/
|
|
22
|
+
import type { Interrupt, Message, ResumeEntry } from "@ag-ui/client";
|
|
23
|
+
export declare const TOOL_APPROVALS_METADATA_KEY = "blocks.toolApprovals";
|
|
24
|
+
export declare const APPROVAL_STATUS_LINE: {
|
|
25
|
+
readonly waiting: "Waiting for approval";
|
|
26
|
+
readonly approved: "Approved";
|
|
27
|
+
readonly rejected: "Rejected";
|
|
28
|
+
};
|
|
29
|
+
/** Appended to a decided card's status while another request of the pause is open. */
|
|
30
|
+
export declare const WAITING_FOR_OTHER_SUFFIX = " \u00B7 waiting for your decision on the other request";
|
|
31
|
+
export type ApprovalDecision = "approve" | "reject";
|
|
32
|
+
export type InterruptAnswer = {
|
|
33
|
+
type: "respond";
|
|
34
|
+
message: unknown;
|
|
35
|
+
} | {
|
|
36
|
+
type: ApprovalDecision;
|
|
37
|
+
};
|
|
38
|
+
export interface InterruptRequest {
|
|
39
|
+
interruptId: string;
|
|
40
|
+
index: number;
|
|
41
|
+
key: string;
|
|
42
|
+
name: string;
|
|
43
|
+
args: Record<string, unknown>;
|
|
44
|
+
allowedDecisions: string[];
|
|
45
|
+
/** The parent transcript call this request paired with; null for a sub-agent's request. */
|
|
46
|
+
toolCallId: string | null;
|
|
47
|
+
/** The transcript call the card hangs on: the call itself, or the waiting `task` call. */
|
|
48
|
+
anchorToolCallId: string | null;
|
|
49
|
+
/** The tool's describe name (`InsertItemsToTasksTable`); `name` is lowercased for the model. */
|
|
50
|
+
displayName: string | null;
|
|
51
|
+
}
|
|
52
|
+
export interface ToolApprovalStamp {
|
|
53
|
+
toolCallId: string;
|
|
54
|
+
decision: ApprovalDecision;
|
|
55
|
+
toolName: string;
|
|
56
|
+
/** Absent on records written before display names. */
|
|
57
|
+
displayName?: string | null;
|
|
58
|
+
args: Record<string, unknown>;
|
|
59
|
+
}
|
|
60
|
+
export interface ApprovalCardView {
|
|
61
|
+
key: string;
|
|
62
|
+
title: string;
|
|
63
|
+
/** The tool input as given; the cards show it as YAML. */
|
|
64
|
+
args: Record<string, unknown>;
|
|
65
|
+
status: "waiting" | "approved" | "rejected";
|
|
66
|
+
statusLine: string;
|
|
67
|
+
/** Set only while this card can still be decided. */
|
|
68
|
+
request: {
|
|
69
|
+
interruptId: string;
|
|
70
|
+
index: number;
|
|
71
|
+
} | null;
|
|
72
|
+
/** Decided here but not sent yet: another card of the same pause is still open. */
|
|
73
|
+
waitingForOthers: boolean;
|
|
74
|
+
}
|
|
75
|
+
export declare const requestKey: (interruptId: string, index: number) => string;
|
|
76
|
+
/**
|
|
77
|
+
* Every request of the open interrupts, in the order the server needs decisions,
|
|
78
|
+
* each paired with its transcript call by order.
|
|
79
|
+
*/
|
|
80
|
+
export declare const requestsOf: (interrupts: readonly Interrupt[], messages: readonly Message[]) => InterruptRequest[];
|
|
81
|
+
export declare const isApprovalRequest: (request: InterruptRequest) => boolean;
|
|
82
|
+
/** Thrown with fixed text when an answer pairs with no open interrupt and is not the welcome seed. */
|
|
83
|
+
export declare const INTERRUPT_NOT_OPEN_ERROR = "answerInterrupt: nothing is waiting for this answer";
|
|
84
|
+
/**
|
|
85
|
+
* A question's answer as HumanInTheLoopMiddleware's `respond` message -- and as the welcome
|
|
86
|
+
* seed's tool result, so both read the same. `?? null`: JSON.stringify(undefined) drops the
|
|
87
|
+
* key, which the server refuses.
|
|
88
|
+
*/
|
|
89
|
+
export declare const respondMessageOf: (answer: unknown) => string;
|
|
90
|
+
/**
|
|
91
|
+
* The welcome seed (`initialUserChoices`): connect writes an ask_user call that no run made,
|
|
92
|
+
* so nothing is parked behind it. Its signature is the one the server used
|
|
93
|
+
* (`answer_orphan_pause`): no user message yet, and the call still waits on the newest
|
|
94
|
+
* assistant message.
|
|
95
|
+
*/
|
|
96
|
+
export declare const isWelcomeSeedCall: (messages: readonly Message[], toolCallId: string) => boolean;
|
|
97
|
+
type AnswerRoute = {
|
|
98
|
+
kind: "respond";
|
|
99
|
+
request: InterruptRequest;
|
|
100
|
+
} | {
|
|
101
|
+
kind: "seed";
|
|
102
|
+
} | {
|
|
103
|
+
kind: "refuse";
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Where a question card's answer goes: the open request it pairs with by order; the seed's
|
|
107
|
+
* tool result when nothing is open; otherwise nowhere -- the caller
|
|
108
|
+
* throws INTERRUPT_NOT_OPEN_ERROR. Never `command.resume`.
|
|
109
|
+
*/
|
|
110
|
+
export declare const answerRouteOf: (interrupts: readonly Interrupt[], messages: readonly Message[], toolCallId: string) => AnswerRoute;
|
|
111
|
+
/**
|
|
112
|
+
* The deprecated `resolveInterrupt(resume)`: a `{decisions}` payload for the ONE
|
|
113
|
+
* open interrupt, as a standard resolved entry. Null when it cannot be routed.
|
|
114
|
+
*/
|
|
115
|
+
export declare const singleInterruptResume: (interrupts: readonly Interrupt[], payload: unknown) => ResumeEntry[] | null;
|
|
116
|
+
/**
|
|
117
|
+
* After a run that carried `resume[]`: anything but RUN_FINISHED (a RUN_ERROR such as a refused
|
|
118
|
+
* resume, a stop, a dropped stream) may have consumed the resume on the server, while
|
|
119
|
+
* @ag-ui/client changes `pendingInterrupts` only on RUN_FINISHED. The chat then forgets them and
|
|
120
|
+
* reconnects; the connect re-sends what is really open.
|
|
121
|
+
*/
|
|
122
|
+
export declare const needsReconnectAfter: (sentResume: boolean, finished: boolean) => boolean;
|
|
123
|
+
/** One `resolved` entry per open interrupt, or null while any request is unanswered. */
|
|
124
|
+
export declare const buildResumeEntries: (interrupts: readonly Interrupt[], requests: readonly InterruptRequest[], answers: ReadonlyMap<string, InterruptAnswer>) => ResumeEntry[] | null;
|
|
125
|
+
/**
|
|
126
|
+
* Typing while the turn is parked: the typed text goes out as a normal
|
|
127
|
+
* message, and each open interrupt gets the protocol's own `cancelled` entry, with no payload.
|
|
128
|
+
* The server rejects every waiting approval with the standard text, skips every question with
|
|
129
|
+
* one fixed marker, and appends the message after them.
|
|
130
|
+
*/
|
|
131
|
+
export declare const cancelledResumeEntries: (interrupts: readonly Interrupt[]) => ResumeEntry[];
|
|
132
|
+
/**
|
|
133
|
+
* Forgets the agent's open interrupts. CopilotKit 1.70.2 keeps
|
|
134
|
+
* `pendingInterrupts` across a thread swap and a reconnect, and @ag-ui/client 0.0.59
|
|
135
|
+
* then refuses the next run AND connect ("Thread has N pending interrupt(s) not
|
|
136
|
+
* addressed by resume"). Call it on a thread swap and right before every connect
|
|
137
|
+
* the chat starts; the connect's own RUN_FINISHED re-sends what is still open.
|
|
138
|
+
*/
|
|
139
|
+
export declare const forgetOpenInterrupts: (agent: unknown) => void;
|
|
140
|
+
/** This browser's own approval decisions, by anchor, until a reload brings the record. */
|
|
141
|
+
export declare const localStampsByAnchor: (requests: readonly InterruptRequest[], answers: ReadonlyMap<string, InterruptAnswer>) => Map<string, ToolApprovalStamp[]>;
|
|
142
|
+
export declare const mergeStamps: (previous: ReadonlyMap<string, ToolApprovalStamp[]>, next: ReadonlyMap<string, ToolApprovalStamp[]>) => ReadonlyMap<string, ToolApprovalStamp[]>;
|
|
143
|
+
/**
|
|
144
|
+
* The decided-here stamps after the chat forgets what is open. A same-thread reload keeps them:
|
|
145
|
+
* the connect's snapshot brings the record, and until it lands nothing else draws the card. A
|
|
146
|
+
* thread or chat swap, or a reconnect after a failed run, clears them.
|
|
147
|
+
*/
|
|
148
|
+
export declare const stampsAfterInterruptReset: (previous: ReadonlyMap<string, ToolApprovalStamp[]>, { keepStamps }: {
|
|
149
|
+
keepStamps: boolean;
|
|
150
|
+
}) => ReadonlyMap<string, ToolApprovalStamp[]>;
|
|
151
|
+
/** Takes back exactly the stamps `mergeStamps` added (by identity), when their send did not go through. */
|
|
152
|
+
export declare const dropStamps: (previous: ReadonlyMap<string, ToolApprovalStamp[]>, added: ReadonlyMap<string, ToolApprovalStamp[]>) => ReadonlyMap<string, ToolApprovalStamp[]>;
|
|
153
|
+
export declare const toolApprovalsOf: (message: unknown) => ToolApprovalStamp[];
|
|
154
|
+
/** `insert_items_in_orders` → `insert items in orders`. */
|
|
155
|
+
export declare const toolWords: (name: string) => string;
|
|
156
|
+
export declare const toolCallOf: (messages: readonly Message[], toolCallId: string) => {
|
|
157
|
+
name: string;
|
|
158
|
+
args: Record<string, unknown>;
|
|
159
|
+
} | null;
|
|
160
|
+
export declare const approvalCardsForToolCall: ({ toolCallId, toolName, toolArgs, toolResult, toolMessage, requests, answers, localStamps, }: {
|
|
161
|
+
toolCallId: string;
|
|
162
|
+
/** The transcript call the card hangs on (`task` for a sub-agent). */
|
|
163
|
+
toolName: string;
|
|
164
|
+
toolArgs: unknown;
|
|
165
|
+
toolResult: string | null;
|
|
166
|
+
toolMessage: unknown;
|
|
167
|
+
requests: readonly InterruptRequest[];
|
|
168
|
+
answers: ReadonlyMap<string, InterruptAnswer>;
|
|
169
|
+
localStamps: ReadonlyMap<string, readonly ToolApprovalStamp[]>;
|
|
170
|
+
}) => ApprovalCardView[];
|
|
171
|
+
/**
|
|
172
|
+
* Whether the tool's normal step renders under its cards: a sub-agent's `task` step
|
|
173
|
+
* always does; a parent call's only once approved AND sent (while waiting it has not
|
|
174
|
+
* run; a rejected call never runs; an approval waiting for the pause's other cards has
|
|
175
|
+
* not been sent yet).
|
|
176
|
+
*/
|
|
177
|
+
export declare const toolStepVisibleBelowCards: (toolName: string, cards: readonly ApprovalCardView[]) => boolean;
|
|
178
|
+
export {};
|
|
179
|
+
//# sourceMappingURL=agui-interrupts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agui-interrupts.d.ts","sourceRoot":"","sources":["../../src/utils/agui-interrupts.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
export const TOOL_APPROVALS_METADATA_KEY = "blocks.toolApprovals";
|
|
2
|
+
const SUBAGENT_TOOL_NAME = "task";
|
|
3
|
+
/**
|
|
4
|
+
* A rejected call never ran; its result is the rejection. The server always uses
|
|
5
|
+
* HumanInTheLoopMiddleware's standard reject (the Reject button, typing, and a new
|
|
6
|
+
* Send Message). Used only when no record of the decision exists: a
|
|
7
|
+
* tool that failed also carries `error`.
|
|
8
|
+
*/
|
|
9
|
+
const REJECTED_RESULT_PREFIXES = ["User rejected the tool call for `"];
|
|
10
|
+
export const APPROVAL_STATUS_LINE = {
|
|
11
|
+
waiting: "Waiting for approval",
|
|
12
|
+
approved: "Approved",
|
|
13
|
+
rejected: "Rejected",
|
|
14
|
+
};
|
|
15
|
+
/** Appended to a decided card's status while another request of the pause is open. */
|
|
16
|
+
export const WAITING_FOR_OTHER_SUFFIX = " · waiting for your decision on the other request";
|
|
17
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18
|
+
const isString = (value) => typeof value === "string";
|
|
19
|
+
const nonEmpty = (value) => (isString(value) && value ? value : null);
|
|
20
|
+
const parseArgs = (raw) => {
|
|
21
|
+
try {
|
|
22
|
+
const parsed = JSON.parse(raw || "{}");
|
|
23
|
+
return isRecord(parsed) ? parsed : {};
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
export const requestKey = (interruptId, index) => `${interruptId}#${index}`;
|
|
30
|
+
/** The name the model sees for a describe name (wf-actions-py `model_tool_name`). */
|
|
31
|
+
const modelToolName = (describeName) => describeName.replace(/[^a-zA-Z0-9_-]/g, "_").toLowerCase();
|
|
32
|
+
/**
|
|
33
|
+
* The server arms each approval with its tool's describe name as the request's
|
|
34
|
+
* `description`. Trusted only when it maps back to the request's name: a pause armed
|
|
35
|
+
* without one (and every pause parked before) carries the middleware's default text.
|
|
36
|
+
*/
|
|
37
|
+
const displayNameOf = (name, description) => isString(description) && description && name && modelToolName(description) === name ? description : null;
|
|
38
|
+
const rawOf = (interrupt) => {
|
|
39
|
+
const metadata = isRecord(interrupt.metadata) ? interrupt.metadata : {};
|
|
40
|
+
const langgraph = isRecord(metadata.langgraph) ? metadata.langgraph : {};
|
|
41
|
+
return isRecord(langgraph.raw) ? langgraph.raw : {};
|
|
42
|
+
};
|
|
43
|
+
const allowedOf = (configs, index, name) => {
|
|
44
|
+
const read = (config) => isRecord(config) && Array.isArray(config.allowed_decisions) ? config.allowed_decisions.filter(isString) : [];
|
|
45
|
+
// The middleware appends one request and one config per gated call, in step.
|
|
46
|
+
const positional = configs[index];
|
|
47
|
+
if (isRecord(positional) && (!name || positional.action_name == null || positional.action_name === name)) {
|
|
48
|
+
return read(positional);
|
|
49
|
+
}
|
|
50
|
+
const byName = configs.find((config) => isRecord(config) && config.action_name === name);
|
|
51
|
+
return byName ? read(byName) : read(positional);
|
|
52
|
+
};
|
|
53
|
+
/** The newest assistant message's calls that have no result yet, in order. */
|
|
54
|
+
const waitingCalls = (messages) => {
|
|
55
|
+
const answered = new Set();
|
|
56
|
+
for (const message of messages) {
|
|
57
|
+
if (message.role === "tool" && "toolCallId" in message && isString(message.toolCallId)) {
|
|
58
|
+
answered.add(message.toolCallId);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
62
|
+
const message = messages[i];
|
|
63
|
+
if (message.role !== "assistant") {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const calls = "toolCalls" in message && Array.isArray(message.toolCalls) ? message.toolCalls : [];
|
|
67
|
+
return calls
|
|
68
|
+
.filter((call) => !answered.has(call.id))
|
|
69
|
+
.map((call) => ({ id: call.id, name: call.function.name, args: parseArgs(call.function.arguments) }));
|
|
70
|
+
}
|
|
71
|
+
return [];
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Every request of the open interrupts, in the order the server needs decisions,
|
|
75
|
+
* each paired with its transcript call by order.
|
|
76
|
+
*/
|
|
77
|
+
export const requestsOf = (interrupts, messages) => {
|
|
78
|
+
const waiting = waitingCalls(messages);
|
|
79
|
+
const tasks = waiting.filter((call) => call.name === SUBAGENT_TOOL_NAME);
|
|
80
|
+
const own = waiting.filter((call) => call.name !== SUBAGENT_TOOL_NAME);
|
|
81
|
+
const host = tasks[0] ?? null;
|
|
82
|
+
const used = new Set();
|
|
83
|
+
const out = [];
|
|
84
|
+
for (const interrupt of interrupts) {
|
|
85
|
+
const raw = rawOf(interrupt);
|
|
86
|
+
const requests = Array.isArray(raw.action_requests) ? raw.action_requests : [];
|
|
87
|
+
const configs = Array.isArray(raw.review_configs) ? raw.review_configs : [];
|
|
88
|
+
requests.forEach((request, index) => {
|
|
89
|
+
const fields = isRecord(request) ? request : {};
|
|
90
|
+
const name = isString(fields.name) ? fields.name : "";
|
|
91
|
+
const match = own.find((call) => call.name === name && !used.has(call.id)) ?? null;
|
|
92
|
+
if (match) {
|
|
93
|
+
used.add(match.id);
|
|
94
|
+
}
|
|
95
|
+
out.push({
|
|
96
|
+
interruptId: interrupt.id,
|
|
97
|
+
index,
|
|
98
|
+
key: requestKey(interrupt.id, index),
|
|
99
|
+
name,
|
|
100
|
+
args: isRecord(fields.args) ? fields.args : {},
|
|
101
|
+
allowedDecisions: allowedOf(configs, index, name),
|
|
102
|
+
toolCallId: match?.id ?? null,
|
|
103
|
+
anchorToolCallId: match ? match.id : (host?.id ?? null),
|
|
104
|
+
displayName: displayNameOf(name, fields.description),
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
};
|
|
110
|
+
export const isApprovalRequest = (request) => request.allowedDecisions.includes("approve");
|
|
111
|
+
/** Thrown with fixed text when an answer pairs with no open interrupt and is not the welcome seed. */
|
|
112
|
+
export const INTERRUPT_NOT_OPEN_ERROR = "answerInterrupt: nothing is waiting for this answer";
|
|
113
|
+
/**
|
|
114
|
+
* A question's answer as HumanInTheLoopMiddleware's `respond` message -- and as the welcome
|
|
115
|
+
* seed's tool result, so both read the same. `?? null`: JSON.stringify(undefined) drops the
|
|
116
|
+
* key, which the server refuses.
|
|
117
|
+
*/
|
|
118
|
+
export const respondMessageOf = (answer) => JSON.stringify(answer ?? null);
|
|
119
|
+
const decisionOf = (answer) => answer.type === "respond" ? { type: "respond", message: respondMessageOf(answer.message) } : { type: answer.type };
|
|
120
|
+
/**
|
|
121
|
+
* The welcome seed (`initialUserChoices`): connect writes an ask_user call that no run made,
|
|
122
|
+
* so nothing is parked behind it. Its signature is the one the server used
|
|
123
|
+
* (`answer_orphan_pause`): no user message yet, and the call still waits on the newest
|
|
124
|
+
* assistant message.
|
|
125
|
+
*/
|
|
126
|
+
export const isWelcomeSeedCall = (messages, toolCallId) => !messages.some((message) => message.role === "user") && waitingCalls(messages).some((call) => call.id === toolCallId);
|
|
127
|
+
/**
|
|
128
|
+
* Where a question card's answer goes: the open request it pairs with by order; the seed's
|
|
129
|
+
* tool result when nothing is open; otherwise nowhere -- the caller
|
|
130
|
+
* throws INTERRUPT_NOT_OPEN_ERROR. Never `command.resume`.
|
|
131
|
+
*/
|
|
132
|
+
export const answerRouteOf = (interrupts, messages, toolCallId) => {
|
|
133
|
+
const request = requestsOf(interrupts, messages).find((candidate) => candidate.toolCallId === toolCallId && !isApprovalRequest(candidate));
|
|
134
|
+
if (request) {
|
|
135
|
+
return { kind: "respond", request };
|
|
136
|
+
}
|
|
137
|
+
if (interrupts.length === 0 && isWelcomeSeedCall(messages, toolCallId)) {
|
|
138
|
+
return { kind: "seed" };
|
|
139
|
+
}
|
|
140
|
+
return { kind: "refuse" };
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* The deprecated `resolveInterrupt(resume)`: a `{decisions}` payload for the ONE
|
|
144
|
+
* open interrupt, as a standard resolved entry. Null when it cannot be routed.
|
|
145
|
+
*/
|
|
146
|
+
export const singleInterruptResume = (interrupts, payload) => interrupts.length === 1 && isRecord(payload) && Array.isArray(payload.decisions)
|
|
147
|
+
? [{ interruptId: interrupts[0].id, status: "resolved", payload: { decisions: payload.decisions } }]
|
|
148
|
+
: null;
|
|
149
|
+
/**
|
|
150
|
+
* After a run that carried `resume[]`: anything but RUN_FINISHED (a RUN_ERROR such as a refused
|
|
151
|
+
* resume, a stop, a dropped stream) may have consumed the resume on the server, while
|
|
152
|
+
* @ag-ui/client changes `pendingInterrupts` only on RUN_FINISHED. The chat then forgets them and
|
|
153
|
+
* reconnects; the connect re-sends what is really open.
|
|
154
|
+
*/
|
|
155
|
+
export const needsReconnectAfter = (sentResume, finished) => sentResume && !finished;
|
|
156
|
+
/** One `resolved` entry per open interrupt, or null while any request is unanswered. */
|
|
157
|
+
export const buildResumeEntries = (interrupts, requests, answers) => {
|
|
158
|
+
if (interrupts.length === 0) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
const entries = [];
|
|
162
|
+
for (const interrupt of interrupts) {
|
|
163
|
+
const decisions = [];
|
|
164
|
+
for (const request of requests.filter((r) => r.interruptId === interrupt.id)) {
|
|
165
|
+
const answer = answers.get(request.key);
|
|
166
|
+
if (!answer) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
decisions.push(decisionOf(answer));
|
|
170
|
+
}
|
|
171
|
+
entries.push({ interruptId: interrupt.id, status: "resolved", payload: { decisions } });
|
|
172
|
+
}
|
|
173
|
+
return entries;
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* Typing while the turn is parked: the typed text goes out as a normal
|
|
177
|
+
* message, and each open interrupt gets the protocol's own `cancelled` entry, with no payload.
|
|
178
|
+
* The server rejects every waiting approval with the standard text, skips every question with
|
|
179
|
+
* one fixed marker, and appends the message after them.
|
|
180
|
+
*/
|
|
181
|
+
export const cancelledResumeEntries = (interrupts) => interrupts.map((interrupt) => ({ interruptId: interrupt.id, status: "cancelled" }));
|
|
182
|
+
/**
|
|
183
|
+
* Forgets the agent's open interrupts. CopilotKit 1.70.2 keeps
|
|
184
|
+
* `pendingInterrupts` across a thread swap and a reconnect, and @ag-ui/client 0.0.59
|
|
185
|
+
* then refuses the next run AND connect ("Thread has N pending interrupt(s) not
|
|
186
|
+
* addressed by resume"). Call it on a thread swap and right before every connect
|
|
187
|
+
* the chat starts; the connect's own RUN_FINISHED re-sends what is still open.
|
|
188
|
+
*/
|
|
189
|
+
export const forgetOpenInterrupts = (agent) => {
|
|
190
|
+
if (agent) {
|
|
191
|
+
agent.pendingInterrupts = [];
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
/** This browser's own approval decisions, by anchor, until a reload brings the record. */
|
|
195
|
+
export const localStampsByAnchor = (requests, answers) => {
|
|
196
|
+
const out = new Map();
|
|
197
|
+
for (const request of requests) {
|
|
198
|
+
const answer = answers.get(request.key);
|
|
199
|
+
if (!answer || answer.type === "respond" || !isApprovalRequest(request) || !request.anchorToolCallId) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const list = out.get(request.anchorToolCallId) ?? [];
|
|
203
|
+
list.push({
|
|
204
|
+
// A sub-agent's call is not in this transcript; its request key stands in.
|
|
205
|
+
toolCallId: request.toolCallId ?? request.key,
|
|
206
|
+
decision: answer.type,
|
|
207
|
+
toolName: request.name,
|
|
208
|
+
displayName: request.displayName,
|
|
209
|
+
args: request.args,
|
|
210
|
+
});
|
|
211
|
+
out.set(request.anchorToolCallId, list);
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
};
|
|
215
|
+
export const mergeStamps = (previous, next) => {
|
|
216
|
+
if (next.size === 0) {
|
|
217
|
+
return previous;
|
|
218
|
+
}
|
|
219
|
+
const merged = new Map(previous);
|
|
220
|
+
next.forEach((stamps, anchor) => merged.set(anchor, [...(merged.get(anchor) ?? []), ...stamps]));
|
|
221
|
+
return merged;
|
|
222
|
+
};
|
|
223
|
+
/**
|
|
224
|
+
* The decided-here stamps after the chat forgets what is open. A same-thread reload keeps them:
|
|
225
|
+
* the connect's snapshot brings the record, and until it lands nothing else draws the card. A
|
|
226
|
+
* thread or chat swap, or a reconnect after a failed run, clears them.
|
|
227
|
+
*/
|
|
228
|
+
export const stampsAfterInterruptReset = (previous, { keepStamps }) => (keepStamps || previous.size === 0 ? previous : new Map());
|
|
229
|
+
/** Takes back exactly the stamps `mergeStamps` added (by identity), when their send did not go through. */
|
|
230
|
+
export const dropStamps = (previous, added) => {
|
|
231
|
+
if (added.size === 0) {
|
|
232
|
+
return previous;
|
|
233
|
+
}
|
|
234
|
+
const kept = new Map();
|
|
235
|
+
previous.forEach((stamps, anchor) => {
|
|
236
|
+
const drop = new Set(added.get(anchor) ?? []);
|
|
237
|
+
const rest = stamps.filter((stamp) => !drop.has(stamp));
|
|
238
|
+
if (rest.length > 0) {
|
|
239
|
+
kept.set(anchor, rest);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
return kept;
|
|
243
|
+
};
|
|
244
|
+
export const toolApprovalsOf = (message) => {
|
|
245
|
+
if (!isRecord(message) || !isRecord(message.metadata)) {
|
|
246
|
+
return [];
|
|
247
|
+
}
|
|
248
|
+
const raw = message.metadata[TOOL_APPROVALS_METADATA_KEY];
|
|
249
|
+
if (!Array.isArray(raw)) {
|
|
250
|
+
return [];
|
|
251
|
+
}
|
|
252
|
+
return raw.flatMap((item) => isRecord(item) &&
|
|
253
|
+
(item.decision === "approve" || item.decision === "reject") &&
|
|
254
|
+
isString(item.toolName) &&
|
|
255
|
+
isString(item.toolCallId)
|
|
256
|
+
? [
|
|
257
|
+
{
|
|
258
|
+
toolCallId: item.toolCallId,
|
|
259
|
+
decision: item.decision,
|
|
260
|
+
toolName: item.toolName,
|
|
261
|
+
displayName: nonEmpty(item.displayName),
|
|
262
|
+
args: isRecord(item.args) ? item.args : {},
|
|
263
|
+
},
|
|
264
|
+
]
|
|
265
|
+
: []);
|
|
266
|
+
};
|
|
267
|
+
const rejectedByResult = (result) => isString(result) && REJECTED_RESULT_PREFIXES.some((prefix) => result.startsWith(prefix));
|
|
268
|
+
/** `insert_items_in_orders` → `insert items in orders`. */
|
|
269
|
+
export const toolWords = (name) => name
|
|
270
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
271
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
|
|
272
|
+
.replace(/[_\s-]+/g, " ")
|
|
273
|
+
.trim()
|
|
274
|
+
.toLowerCase();
|
|
275
|
+
export const toolCallOf = (messages, toolCallId) => {
|
|
276
|
+
for (const message of messages) {
|
|
277
|
+
if (message.role !== "assistant" || !("toolCalls" in message) || !Array.isArray(message.toolCalls)) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const call = message.toolCalls.find((candidate) => candidate.id === toolCallId);
|
|
281
|
+
if (call) {
|
|
282
|
+
return { name: call.function.name, args: parseArgs(call.function.arguments) };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
};
|
|
287
|
+
const titleOf = (toolName) => {
|
|
288
|
+
const words = toolWords(toolName);
|
|
289
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
290
|
+
};
|
|
291
|
+
const statusOf = (decision) => decision === "approve" ? "approved" : "rejected";
|
|
292
|
+
export const approvalCardsForToolCall = ({ toolCallId, toolName, toolArgs, toolResult, toolMessage, requests, answers, localStamps, }) => {
|
|
293
|
+
const live = requests.filter((request) => request.anchorToolCallId === toolCallId && isApprovalRequest(request));
|
|
294
|
+
if (live.length > 0) {
|
|
295
|
+
const othersOpen = requests.some((request) => !answers.has(request.key));
|
|
296
|
+
return live.map((request) => {
|
|
297
|
+
const answer = answers.get(request.key);
|
|
298
|
+
const status = answer && answer.type !== "respond" ? statusOf(answer.type) : "waiting";
|
|
299
|
+
const waitingForOthers = status !== "waiting" && othersOpen;
|
|
300
|
+
return {
|
|
301
|
+
key: request.key,
|
|
302
|
+
title: titleOf(request.displayName ?? request.name),
|
|
303
|
+
args: request.args,
|
|
304
|
+
status,
|
|
305
|
+
statusLine: waitingForOthers
|
|
306
|
+
? `${APPROVAL_STATUS_LINE[status]}${WAITING_FOR_OTHER_SUFFIX}`
|
|
307
|
+
: APPROVAL_STATUS_LINE[status],
|
|
308
|
+
request: status === "waiting" ? { interruptId: request.interruptId, index: request.index } : null,
|
|
309
|
+
waitingForOthers,
|
|
310
|
+
};
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
// The server's record wins once a reload brought it; until then, this browser's own.
|
|
314
|
+
const recorded = toolApprovalsOf(toolMessage);
|
|
315
|
+
const decided = recorded.length > 0 ? recorded : (localStamps.get(toolCallId) ?? []);
|
|
316
|
+
if (decided.length > 0) {
|
|
317
|
+
return decided.map((stamp) => {
|
|
318
|
+
const status = statusOf(stamp.decision);
|
|
319
|
+
return {
|
|
320
|
+
key: `${toolCallId}#${stamp.toolCallId}`,
|
|
321
|
+
title: titleOf(stamp.displayName ?? stamp.toolName),
|
|
322
|
+
args: stamp.args,
|
|
323
|
+
status,
|
|
324
|
+
statusLine: APPROVAL_STATUS_LINE[status],
|
|
325
|
+
request: null,
|
|
326
|
+
waitingForOthers: false,
|
|
327
|
+
};
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
if (toolName !== SUBAGENT_TOOL_NAME && rejectedByResult(toolResult)) {
|
|
331
|
+
return [
|
|
332
|
+
{
|
|
333
|
+
key: `${toolCallId}#rejected`,
|
|
334
|
+
title: titleOf(toolName),
|
|
335
|
+
args: isRecord(toolArgs) ? toolArgs : {},
|
|
336
|
+
status: "rejected",
|
|
337
|
+
statusLine: APPROVAL_STATUS_LINE.rejected,
|
|
338
|
+
request: null,
|
|
339
|
+
waitingForOthers: false,
|
|
340
|
+
},
|
|
341
|
+
];
|
|
342
|
+
}
|
|
343
|
+
return [];
|
|
344
|
+
};
|
|
345
|
+
/**
|
|
346
|
+
* Whether the tool's normal step renders under its cards: a sub-agent's `task` step
|
|
347
|
+
* always does; a parent call's only once approved AND sent (while waiting it has not
|
|
348
|
+
* run; a rejected call never runs; an approval waiting for the pause's other cards has
|
|
349
|
+
* not been sent yet).
|
|
350
|
+
*/
|
|
351
|
+
export const toolStepVisibleBelowCards = (toolName, cards) => toolName === SUBAGENT_TOOL_NAME || cards.every((card) => card.status === "approved" && !card.waitingForOthers);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blocksdiy/react-common",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.52.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "React common",
|
|
6
6
|
"keywords": [],
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"penpal": "^8.0.0",
|
|
40
40
|
"react": "^19.2.8",
|
|
41
41
|
"use-stick-to-bottom": "1.1.4",
|
|
42
|
-
"@blocksdiy/blocks-client-api": "1.
|
|
42
|
+
"@blocksdiy/blocks-client-api": "1.25.2"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/react": "^19.2.18",
|