@adminforth/agent 2.2.5 → 2.3.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/application/ports.d.ts +18 -0
- package/dist/application/runTurnUseCase.d.ts +17 -1
- package/dist/application/runTurnUseCase.js +145 -4
- package/dist/custom/ChatFooter.vue +61 -23
- package/dist/custom/ChatSurface.vue +1 -0
- package/dist/custom/composables/agentStore/useAgentChat.ts +122 -7
- package/dist/custom/composables/agentStore/useAgentSessions.ts +8 -5
- package/dist/custom/composables/agentStore/useAgentSteerQueue.ts +5 -1
- package/dist/custom/composables/useAgentStore.ts +83 -0
- package/dist/custom/conversation_area/MessageRenderer.vue +114 -29
- package/dist/custom/conversation_area/TextRenderer.vue +16 -6
- package/dist/custom/speech_recognition_frontend/MicrophoneButon.vue +4 -0
- package/dist/custom/types.ts +7 -0
- package/dist/domain/agentEvents.d.ts +4 -0
- package/dist/domain/turnTypes.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/llm/agentModels.d.ts +1 -0
- package/dist/llm/agentRuntime.d.ts +14 -0
- package/dist/llm/agentRuntime.js +42 -3
- package/dist/llm/langGraphLlm.d.ts +8 -0
- package/dist/llm/langGraphLlm.js +12 -0
- package/dist/persistence/sessionStore.d.ts +30 -0
- package/dist/persistence/sessionStore.js +56 -1
- package/dist/transport/http/context.d.ts +4 -2
- package/dist/transport/http/coreEndpoints.js +35 -0
- package/dist/transport/http/sessionEndpoints.js +3 -0
- package/dist/transport/sse/sseWriter.js +12 -0
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
|
@@ -34,6 +34,14 @@ export const useAgentStore = defineStore('agent', () => {
|
|
|
34
34
|
const textInput = ref<HTMLTextAreaElement | null>(null);
|
|
35
35
|
const userMessageInput = ref();
|
|
36
36
|
const trimmedUserMessage = computed(() => userMessageInput.value ? userMessageInput.value.trim() : '');
|
|
37
|
+
// Message-edit state. `editingEnabled` mirrors the server capability (checkpointer +
|
|
38
|
+
// turn checkpoints). While a message is being edited its id/turnId are held here and
|
|
39
|
+
// the user's in-progress input is stashed in `savedUserInput` so it can be restored.
|
|
40
|
+
const editingEnabled = ref(false);
|
|
41
|
+
const editingMessageId = ref<string | null>(null);
|
|
42
|
+
const editingMessageTurnId = ref<string | null>(null);
|
|
43
|
+
const savedUserInput = ref('');
|
|
44
|
+
const isEditingMessage = computed(() => editingMessageId.value !== null);
|
|
37
45
|
const lastMessage = ref('');
|
|
38
46
|
const isTeleportedToBody = ref(false);
|
|
39
47
|
const setIsTeleportedToBody = (isTeleported: boolean) => {
|
|
@@ -55,6 +63,7 @@ export const useAgentStore = defineStore('agent', () => {
|
|
|
55
63
|
setCurrentChat,
|
|
56
64
|
abortCurrentChatRequest,
|
|
57
65
|
submitToolApproval: submitToolApprovalResponse,
|
|
66
|
+
sendEditMessage,
|
|
58
67
|
} = createAgentChatManager({
|
|
59
68
|
lastMessage,
|
|
60
69
|
activeModeName,
|
|
@@ -275,6 +284,10 @@ export const useAgentStore = defineStore('agent', () => {
|
|
|
275
284
|
}
|
|
276
285
|
})
|
|
277
286
|
watch(activeSessionId, (newVal: string | null) => {
|
|
287
|
+
// The edited message lives in the outgoing session's chat, so drop edit mode.
|
|
288
|
+
if (editingMessageId.value !== null) {
|
|
289
|
+
exitEditMode();
|
|
290
|
+
}
|
|
278
291
|
if (newVal) {
|
|
279
292
|
setLocalStorageItem('lastSessionId', newVal);
|
|
280
293
|
}
|
|
@@ -448,6 +461,67 @@ export const useAgentStore = defineStore('agent', () => {
|
|
|
448
461
|
addSystemMessage(RESERVED_SYSTEM_MESSAGE_CONTENT.AGENT_RESPONSE_ABORTED);
|
|
449
462
|
}
|
|
450
463
|
|
|
464
|
+
function setEditingEnabled(enabled: boolean) {
|
|
465
|
+
editingEnabled.value = enabled;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Enter edit mode for a user message: stash the current input, then load the
|
|
469
|
+
// message's text into the textarea for the user to amend. Allowed mid-generation —
|
|
470
|
+
// committing the edit stops the running turn (see submitEditMessage).
|
|
471
|
+
function startEditMessage(message: any) {
|
|
472
|
+
if (!editingEnabled.value) {
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
const turnId = message?.metadata?.turnId;
|
|
476
|
+
const messageId = message?.id;
|
|
477
|
+
if (!turnId || !messageId) {
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
if (editingMessageId.value === null) {
|
|
481
|
+
savedUserInput.value = userMessageInput.value ?? '';
|
|
482
|
+
}
|
|
483
|
+
const text = (message.parts ?? [])
|
|
484
|
+
.filter((part: any) => part.type === 'text')
|
|
485
|
+
.map((part: any) => part.text ?? '')
|
|
486
|
+
.join('');
|
|
487
|
+
editingMessageId.value = messageId;
|
|
488
|
+
editingMessageTurnId.value = turnId;
|
|
489
|
+
userMessageInput.value = text;
|
|
490
|
+
nextTick(() => focusTextInput());
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Leave edit mode and restore whatever the user had been typing before.
|
|
494
|
+
function exitEditMode() {
|
|
495
|
+
userMessageInput.value = savedUserInput.value;
|
|
496
|
+
savedUserInput.value = '';
|
|
497
|
+
editingMessageId.value = null;
|
|
498
|
+
editingMessageTurnId.value = null;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function cancelEditMessage() {
|
|
502
|
+
if (editingMessageId.value === null) {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
exitEditMode();
|
|
506
|
+
nextTick(() => focusTextInput());
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async function submitEditMessage() {
|
|
510
|
+
if (editingMessageId.value === null) {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
const text = trimmedUserMessage.value;
|
|
514
|
+
if (!text) {
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
const messageId = editingMessageId.value;
|
|
518
|
+
const turnId = editingMessageTurnId.value!;
|
|
519
|
+
steerBuffer.clear();
|
|
520
|
+
// Restore the stashed input and leave edit mode before the regeneration streams in.
|
|
521
|
+
exitEditMode();
|
|
522
|
+
await sendEditMessage({ messageId, turnId, text });
|
|
523
|
+
}
|
|
524
|
+
|
|
451
525
|
function resolveInternalRoute(href: string): string | null {
|
|
452
526
|
if (href.startsWith('#')) {
|
|
453
527
|
return `${window.location.pathname}${window.location.search}${href}`;
|
|
@@ -533,6 +607,15 @@ export const useAgentStore = defineStore('agent', () => {
|
|
|
533
607
|
getLocalStorageItem,
|
|
534
608
|
addDebugMessage,
|
|
535
609
|
abortCurrentChatRequestAndAddSystemMessage,
|
|
610
|
+
//_________-Message editing-_____________
|
|
611
|
+
editingEnabled,
|
|
612
|
+
setEditingEnabled,
|
|
613
|
+
isEditingMessage,
|
|
614
|
+
editingMessageId,
|
|
615
|
+
startEditMessage,
|
|
616
|
+
cancelEditMessage,
|
|
617
|
+
submitEditMessage,
|
|
618
|
+
//_______________________________________
|
|
536
619
|
addSystemMessage,
|
|
537
620
|
isAudioChatMode,
|
|
538
621
|
setIsAudioChatMode,
|
|
@@ -1,33 +1,77 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
3
|
-
|
|
4
|
-
class="self-end mr-4 mb-0.5 text-xs italic text-gray-400 dark:text-gray-500"
|
|
2
|
+
<div
|
|
3
|
+
class="relative flex flex-col w-full group/msg"
|
|
5
4
|
>
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
5
|
+
<div
|
|
6
|
+
v-if="canEdit"
|
|
7
|
+
class="absolute w-full h-[calc(100%+1.5rem)] pb-8"
|
|
8
|
+
@mouseenter="showEditButtonOnHover"
|
|
9
|
+
@mouseleave="hideEditButtonOnHover"
|
|
10
|
+
/>
|
|
11
|
+
<Transition
|
|
12
|
+
enter-active-class="transition ease-out duration-200"
|
|
13
|
+
enter-from-class="opacity-0 scale-95"
|
|
14
|
+
enter-to-class="opacity-100 scale-100"
|
|
15
|
+
leave-active-class="transition ease-in duration-150"
|
|
16
|
+
leave-from-class="opacity-100 scale-100"
|
|
17
|
+
leave-to-class="opacity-0 scale-95"
|
|
18
|
+
>
|
|
19
|
+
<div
|
|
20
|
+
v-if="canEdit && showEditButton"
|
|
21
|
+
class="absolute flex -bottom-6 right-0 self-end p-1 mr-4 mt-0.5 z-20"
|
|
22
|
+
@mouseenter="showEditButtonOnHover"
|
|
23
|
+
@mouseleave="hideEditButtonOnHover"
|
|
24
|
+
>
|
|
25
|
+
<button
|
|
26
|
+
type="button"
|
|
27
|
+
class="flex items-center justify-center w-8 h-8 rounded-md text-gray-400 hover:text-lightPrimary dark:hover:text-darkPrimary opacity-60 group-hover/msg:opacity-100 transition-opacity duration-150 hover:scale-110"
|
|
28
|
+
:title="$t('Copy message')"
|
|
29
|
+
@click="copyMessageToClipboard(message)"
|
|
30
|
+
>
|
|
31
|
+
<IconFileCopySolid class="w-5 h-5" />
|
|
32
|
+
</button>
|
|
33
|
+
<button
|
|
34
|
+
type="button"
|
|
35
|
+
class="flex items-center justify-center w-8 h-8 rounded-md text-gray-400 hover:text-lightPrimary dark:hover:text-darkPrimary opacity-60 group-hover/msg:opacity-100 transition-opacity duration-150 hover:scale-110"
|
|
36
|
+
:title="$t('Edit message')"
|
|
37
|
+
@click="agentStore.startEditMessage(message)"
|
|
38
|
+
>
|
|
39
|
+
<IconPenSolid class="w-5 h-5" />
|
|
40
|
+
</button>
|
|
41
|
+
</div>
|
|
42
|
+
</Transition>
|
|
43
|
+
<div
|
|
44
|
+
v-if="message.role === 'user' && message.metadata?.steer"
|
|
45
|
+
class="self-end mr-4 mb-0.5 text-xs italic text-gray-400 dark:text-gray-500"
|
|
46
|
+
>
|
|
47
|
+
{{ $t('Steering instruction') }}
|
|
48
|
+
</div>
|
|
49
|
+
<ProcessingTimeline
|
|
50
|
+
:message="message"
|
|
51
|
+
:isLastMessageInChat="isLastMessageInChat"
|
|
25
52
|
/>
|
|
26
|
-
<
|
|
27
|
-
v-
|
|
28
|
-
:
|
|
29
|
-
|
|
30
|
-
|
|
53
|
+
<template
|
|
54
|
+
v-for="(part, index) in getMessageParts(message)"
|
|
55
|
+
:key="part.type"
|
|
56
|
+
>
|
|
57
|
+
<TextRenderer
|
|
58
|
+
v-if="part.type === 'text' && !checkIfMessageSystemMessage(part.text ?? '')"
|
|
59
|
+
:message="part.text"
|
|
60
|
+
:role="props.message.role"
|
|
61
|
+
:highlight="isBeingEdited"
|
|
62
|
+
:state="part.state ?? (props.message.role === 'user' ? 'done' : undefined)"
|
|
63
|
+
/>
|
|
64
|
+
<ToolApprovalRenderer
|
|
65
|
+
v-else-if="part.type === 'data-tool-approval'"
|
|
66
|
+
:data="part.data ?? {}"
|
|
67
|
+
/>
|
|
68
|
+
<SystemMessageRenderer
|
|
69
|
+
v-else-if="part.type === 'text'"
|
|
70
|
+
:message="part.text"
|
|
71
|
+
/>
|
|
72
|
+
</template>
|
|
73
|
+
|
|
74
|
+
</div>
|
|
31
75
|
|
|
32
76
|
</template>
|
|
33
77
|
|
|
@@ -35,6 +79,8 @@
|
|
|
35
79
|
|
|
36
80
|
|
|
37
81
|
<script setup lang="ts">
|
|
82
|
+
import { computed, ref } from 'vue';
|
|
83
|
+
import { IconPenSolid, IconFileCopySolid } from '@iconify-prerendered/vue-flowbite';
|
|
38
84
|
import TextRenderer from './TextRenderer.vue';
|
|
39
85
|
import type { IMessage } from '../types';
|
|
40
86
|
import { getMessageParts } from '../utils';
|
|
@@ -42,14 +88,53 @@
|
|
|
42
88
|
import SystemMessageRenderer from './SystemMessageRenderer.vue';
|
|
43
89
|
import ToolApprovalRenderer from './ToolApprovalRenderer.vue';
|
|
44
90
|
import { RESERVED_SYSTEM_MESSAGE_CONTENT } from '../composables/agentStore/constants';
|
|
91
|
+
import { useAgentStore } from '../composables/useAgentStore';
|
|
92
|
+
import { useAdminforth } from '@/adminforth';
|
|
45
93
|
|
|
46
|
-
const props = defineProps<{
|
|
47
|
-
message: IMessage
|
|
94
|
+
const props = defineProps<{
|
|
95
|
+
message: IMessage
|
|
48
96
|
isLastMessageInChat: boolean
|
|
49
97
|
}>();
|
|
50
98
|
|
|
99
|
+
const agentStore = useAgentStore();
|
|
100
|
+
const { alert } = useAdminforth();
|
|
101
|
+
const showEditButton = ref(false);
|
|
102
|
+
// A user message is editable once it carries a turnId (loaded from history or stamped
|
|
103
|
+
// after its turn finished). Steer sub-messages and other roles are never editable, and
|
|
104
|
+
// we hide the affordance entirely while another edit or a live turn is in flight.
|
|
105
|
+
const canEdit = computed(() =>
|
|
106
|
+
agentStore.editingEnabled
|
|
107
|
+
&& props.message.role === 'user'
|
|
108
|
+
&& !props.message.metadata?.steer
|
|
109
|
+
&& Boolean(props.message.metadata?.turnId)
|
|
110
|
+
&& !agentStore.isEditingMessage
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const isBeingEdited = computed(() =>
|
|
114
|
+
Boolean(props.message.id) && props.message.id === agentStore.editingMessageId
|
|
115
|
+
);
|
|
116
|
+
|
|
51
117
|
function checkIfMessageSystemMessage(message: IMessage): boolean {
|
|
52
118
|
const isReserved = Object.values(RESERVED_SYSTEM_MESSAGE_CONTENT).includes(message as RESERVED_SYSTEM_MESSAGE_CONTENT);
|
|
53
119
|
return isReserved;
|
|
54
120
|
}
|
|
121
|
+
|
|
122
|
+
function showEditButtonOnHover() {
|
|
123
|
+
showEditButton.value = true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function hideEditButtonOnHover() {
|
|
127
|
+
showEditButton.value = false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function copyMessageToClipboard(message: IMessage) {
|
|
131
|
+
navigator.clipboard.writeText(message.parts[0].text ?? '')
|
|
132
|
+
.then(() => {
|
|
133
|
+
alert({ message: 'Message copied to clipboard', variant: 'success' });
|
|
134
|
+
})
|
|
135
|
+
.catch((error) => {
|
|
136
|
+
console.error('Failed to copy message to clipboard:', error);
|
|
137
|
+
alert({ message: 'Failed to copy message to clipboard', variant: 'danger' });
|
|
138
|
+
});
|
|
139
|
+
}
|
|
55
140
|
</script>
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div
|
|
3
|
-
class="flex items-center justify-center rounded-xl border px-6 border-gray-200 dark:border-gray-700 min-w-0"
|
|
3
|
+
class="flex items-center justify-center rounded-xl border px-6 border-gray-200 dark:border-gray-700 min-w-0 transition-shadow duration-150"
|
|
4
4
|
@click="handleMarkdownLinkClick"
|
|
5
5
|
:class="[
|
|
6
|
-
hasVegaLite ? 'w-full my-2' : '',
|
|
6
|
+
hasVegaLite ? 'w-full my-2' : 'm-2',
|
|
7
7
|
props.role === 'user' ? 'bg-lightListTableHeading dark:bg-darkListTableHeading self-end max-w-[80%] mr-4'
|
|
8
|
-
: 'border-none self-start max-w-full'
|
|
8
|
+
: 'border-none self-start max-w-full',
|
|
9
|
+
props.highlight ? 'ring-2 ring-lightPrimary dark:ring-darkPrimary w-full max-w-[calc(100%-2rem)]' : ''
|
|
9
10
|
]"
|
|
10
11
|
>
|
|
11
12
|
<IncremarkContent
|
|
12
13
|
class="text-wrap break-words w-full max-w-full"
|
|
13
14
|
v-if="content"
|
|
14
|
-
:
|
|
15
|
-
:
|
|
15
|
+
:key="renderNonce"
|
|
16
|
+
:content="content"
|
|
17
|
+
:is-finished="isFinished"
|
|
16
18
|
:components="incremarkComponents"
|
|
17
19
|
:incremark-options="incremarkOptions"
|
|
18
20
|
/>
|
|
@@ -33,7 +35,8 @@
|
|
|
33
35
|
const props = defineProps<{
|
|
34
36
|
message: string | undefined,
|
|
35
37
|
state: string | undefined,
|
|
36
|
-
role: IMessage['role']
|
|
38
|
+
role: IMessage['role'],
|
|
39
|
+
highlight?: boolean
|
|
37
40
|
}>();
|
|
38
41
|
|
|
39
42
|
const emit = defineEmits(['toggle-thoughts']);
|
|
@@ -49,6 +52,13 @@
|
|
|
49
52
|
|
|
50
53
|
const content = computed(() => props.message)
|
|
51
54
|
const isFinished = computed(() => props.state === 'done')
|
|
55
|
+
|
|
56
|
+
const renderNonce = ref(0);
|
|
57
|
+
watch(content, (newContent, oldContent) => {
|
|
58
|
+
if (isFinished.value && newContent !== oldContent) {
|
|
59
|
+
renderNonce.value++;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
52
62
|
const hasVegaLite = computed(() => props.message?.includes('```vega-lite'))
|
|
53
63
|
const isStateStreaming = computed(() => props.state === 'streaming')
|
|
54
64
|
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
:isRecording="microphoneButtonMode === 'listen'"
|
|
28
28
|
:amplitude="audioAmplitude"
|
|
29
29
|
/>
|
|
30
|
+
<Spinner v-if="isPolling" class="ml-2 w-4 h-4 text-lightButtonsText dark:text-darkButtonsText fill-lightButtonsBackground dark:fill-darkPrimary" />
|
|
30
31
|
</div>
|
|
31
32
|
<div v-else-if="microphoneButtonMode === 'generating'" class="flex items-center justify-center gap-2 text-white text-sm">
|
|
32
33
|
<span class="w-3 h-3 bg-white rounded-sm" />
|
|
@@ -65,6 +66,7 @@ const hideAnimationDebounced = debounce(() => {
|
|
|
65
66
|
const sendUserRecordDebounced = debounce(() => {
|
|
66
67
|
sendRecordForTranscription();
|
|
67
68
|
}, 500);
|
|
69
|
+
const isPolling = ref(false);
|
|
68
70
|
|
|
69
71
|
const isAudioChatMode = computed(() => agentStore.isAudioChatMode);
|
|
70
72
|
|
|
@@ -116,8 +118,10 @@ function toggleChatMode() {
|
|
|
116
118
|
}
|
|
117
119
|
|
|
118
120
|
async function onStartRecording() {
|
|
121
|
+
isPolling.value = true;
|
|
119
122
|
await unlockAudio();
|
|
120
123
|
await requestMicAndStartVAD(saidSomething, stopRecording, onAnySound);
|
|
124
|
+
isPolling.value = false;
|
|
121
125
|
microphoneButtonMode.value = 'listen';
|
|
122
126
|
agentAudio.playBeep(1000);
|
|
123
127
|
}
|
package/dist/custom/types.ts
CHANGED
|
@@ -30,11 +30,18 @@ export interface IToolGroup {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export interface IMessage {
|
|
33
|
+
id?: string;
|
|
33
34
|
role: 'user' | 'assistant' | 'system';
|
|
34
35
|
metadata?: any,
|
|
35
36
|
parts: IPart[];
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
export interface IStoredMessage {
|
|
40
|
+
role: 'user' | 'assistant' | 'system';
|
|
41
|
+
text: string;
|
|
42
|
+
turnId?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
38
45
|
export interface IAgentSession {
|
|
39
46
|
sessionId: string;
|
|
40
47
|
title: string;
|
|
@@ -63,6 +63,7 @@ export type RunAndPersistAgentResponseInput = BaseAgentTurnInput & {
|
|
|
63
63
|
approvalDecision?: "approve" | "reject";
|
|
64
64
|
failureLogMessage: string;
|
|
65
65
|
abortLogMessage: string;
|
|
66
|
+
editTurnId?: string;
|
|
66
67
|
};
|
|
67
68
|
export type RunAndPersistAgentResponseResult = {
|
|
68
69
|
text: string;
|
|
@@ -72,6 +73,9 @@ export type RunAndPersistAgentResponseResult = {
|
|
|
72
73
|
};
|
|
73
74
|
export type HandleTurnInput = TextAgentTurnInput;
|
|
74
75
|
export type HandleSpeechTurnInput = SpeechAgentTurnInput;
|
|
76
|
+
export type HandleEditTurnInput = TextAgentTurnInput & {
|
|
77
|
+
turnId: string;
|
|
78
|
+
};
|
|
75
79
|
/**
|
|
76
80
|
* Provider-agnostic message passed from the application layer to the LLM port.
|
|
77
81
|
* The infrastructure adapter maps it onto concrete LangChain messages.
|
package/dist/index.js
CHANGED
|
@@ -79,6 +79,8 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
|
|
|
79
79
|
getAdminforth: () => this.adminforth,
|
|
80
80
|
getAgentSystemPrompt: this.getAgentSystemPrompt.bind(this),
|
|
81
81
|
hasPersistentCheckpointer: Boolean(this.options.checkpointResource),
|
|
82
|
+
turnCheckpointsEnabled: Boolean(this.options.turnResource.checkpointIdField),
|
|
83
|
+
sessionResource: this.options.sessionResource,
|
|
82
84
|
createDebugSink: createSequenceDebugCollector,
|
|
83
85
|
});
|
|
84
86
|
this.speechTurnService = new SpeechTurnService(this.runTurnUseCase.runAndPersistAgentResponse.bind(this.runTurnUseCase));
|
|
@@ -107,6 +109,7 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
|
|
|
107
109
|
defaultModeName: this.options.modes[0].name,
|
|
108
110
|
stickByDefault: (_b = this.options.stickByDefault) !== null && _b !== void 0 ? _b : false,
|
|
109
111
|
hasAudioAdapter: Boolean(this.options.audioAdapter),
|
|
112
|
+
editingEnabled: Boolean(this.options.checkpointResource && this.options.turnResource.checkpointIdField),
|
|
110
113
|
}
|
|
111
114
|
});
|
|
112
115
|
if (!this.adminforth.config.customization.customHeadItems) {
|
|
@@ -149,6 +152,7 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
|
|
|
149
152
|
adminforth: this.adminforth,
|
|
150
153
|
options: this.options,
|
|
151
154
|
handleTurn: this.runTurnUseCase.handleTurn.bind(this.runTurnUseCase),
|
|
155
|
+
handleEditTurn: this.runTurnUseCase.handleEditTurn.bind(this.runTurnUseCase),
|
|
152
156
|
handleSpeechTurn: this.speechTurnService.handle.bind(this.speechTurnService),
|
|
153
157
|
steer: ({ sessionId, message }) => {
|
|
154
158
|
const { id } = this.steerBuffer.add(sessionId, message);
|
|
@@ -24,4 +24,18 @@ export declare class AgentRuntime {
|
|
|
24
24
|
models: AgentTurnModels;
|
|
25
25
|
sessionId: string;
|
|
26
26
|
}): Promise<unknown[]>;
|
|
27
|
+
/**
|
|
28
|
+
* Return the thread's latest (tip) checkpoint id, or null when the thread has no
|
|
29
|
+
* persisted state. Builds a minimal agent and reads its state — no model call is
|
|
30
|
+
* made. Called after a turn completes to record the turn's fork point for editing.
|
|
31
|
+
*/
|
|
32
|
+
getLatestCheckpointId(input: {
|
|
33
|
+
models: AgentTurnModels;
|
|
34
|
+
sessionId: string;
|
|
35
|
+
}): Promise<string | null>;
|
|
36
|
+
/**
|
|
37
|
+
* Drop the entire LangGraph checkpoint thread for a session. Used when editing the
|
|
38
|
+
* first message, where there is no earlier checkpoint to fork from.
|
|
39
|
+
*/
|
|
40
|
+
resetThreadCheckpoints(sessionId: string): Promise<void>;
|
|
27
41
|
}
|
package/dist/llm/agentRuntime.js
CHANGED
|
@@ -68,9 +68,9 @@ export class AgentRuntime {
|
|
|
68
68
|
recursionLimit: 100,
|
|
69
69
|
callbacks: [createAgentLlmMetricsLogger()],
|
|
70
70
|
signal: input.context.abortSignal,
|
|
71
|
-
configurable: {
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
configurable: Object.assign({ thread_id: input.context.sessionId }, (input.branchFromCheckpointId
|
|
72
|
+
? { checkpoint_id: input.branchFromCheckpointId }
|
|
73
|
+
: {})),
|
|
74
74
|
context: toLangchainAgentContext(Object.assign(Object.assign({}, input.context), { adminBaseUrl: adminforth.config.baseUrlSlashed, emit: input.observability.emit, sequenceDebugSink })),
|
|
75
75
|
});
|
|
76
76
|
});
|
|
@@ -104,4 +104,43 @@ export class AgentRuntime {
|
|
|
104
104
|
return tasks.flatMap((task) => (Array.isArray(task === null || task === void 0 ? void 0 : task.interrupts) ? task.interrupts : []));
|
|
105
105
|
});
|
|
106
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Return the thread's latest (tip) checkpoint id, or null when the thread has no
|
|
109
|
+
* persisted state. Builds a minimal agent and reads its state — no model call is
|
|
110
|
+
* made. Called after a turn completes to record the turn's fork point for editing.
|
|
111
|
+
*/
|
|
112
|
+
getLatestCheckpointId(input) {
|
|
113
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
114
|
+
var _a, _b;
|
|
115
|
+
const apiBasedTools = this.options.toolProvider.getApiBasedTools();
|
|
116
|
+
const tools = yield this.options.toolProvider.getTools(apiBasedTools);
|
|
117
|
+
const agent = createAgent({
|
|
118
|
+
name: this.options.name,
|
|
119
|
+
model: input.models.model,
|
|
120
|
+
checkpointer: this.options.getCheckpointer(),
|
|
121
|
+
tools,
|
|
122
|
+
contextSchema,
|
|
123
|
+
middleware: [
|
|
124
|
+
humanInTheLoopMiddleware({
|
|
125
|
+
interruptOn: createHumanInTheLoopInterrupts(apiBasedTools),
|
|
126
|
+
descriptionPrefix: "Tool execution pending approval",
|
|
127
|
+
}),
|
|
128
|
+
],
|
|
129
|
+
});
|
|
130
|
+
const state = yield agent.getState({
|
|
131
|
+
configurable: { thread_id: input.sessionId },
|
|
132
|
+
});
|
|
133
|
+
const checkpointId = (_b = (_a = state === null || state === void 0 ? void 0 : state.config) === null || _a === void 0 ? void 0 : _a.configurable) === null || _b === void 0 ? void 0 : _b.checkpoint_id;
|
|
134
|
+
return checkpointId ? String(checkpointId) : null;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Drop the entire LangGraph checkpoint thread for a session. Used when editing the
|
|
139
|
+
* first message, where there is no earlier checkpoint to fork from.
|
|
140
|
+
*/
|
|
141
|
+
resetThreadCheckpoints(sessionId) {
|
|
142
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
143
|
+
yield this.options.getCheckpointer().deleteThread(sessionId);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
107
146
|
}
|
|
@@ -22,4 +22,12 @@ export declare class LangGraphLlm implements LlmPort {
|
|
|
22
22
|
completionAdapter: AgentModeCompletionAdapter;
|
|
23
23
|
sessionId: string;
|
|
24
24
|
}): Promise<PendingInterrupt[]>;
|
|
25
|
+
getLatestCheckpointId(input: {
|
|
26
|
+
completionAdapter: AgentModeCompletionAdapter;
|
|
27
|
+
sessionId: string;
|
|
28
|
+
}): Promise<string | null>;
|
|
29
|
+
resetThreadCheckpoints(input: {
|
|
30
|
+
completionAdapter: AgentModeCompletionAdapter;
|
|
31
|
+
sessionId: string;
|
|
32
|
+
}): Promise<void>;
|
|
25
33
|
}
|
package/dist/llm/langGraphLlm.js
CHANGED
|
@@ -37,6 +37,7 @@ export class LangGraphLlm {
|
|
|
37
37
|
input: runtimeInput,
|
|
38
38
|
context: input.context,
|
|
39
39
|
observability: input.observability,
|
|
40
|
+
branchFromCheckpointId: input.branchFromCheckpointId,
|
|
40
41
|
});
|
|
41
42
|
return adaptRawStream(rawStream);
|
|
42
43
|
});
|
|
@@ -51,4 +52,15 @@ export class LangGraphLlm {
|
|
|
51
52
|
return normalizeInterrupts(raw);
|
|
52
53
|
});
|
|
53
54
|
}
|
|
55
|
+
getLatestCheckpointId(input) {
|
|
56
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
57
|
+
const models = yield this.modelFactory.create(input.completionAdapter);
|
|
58
|
+
return this.runtime.getLatestCheckpointId({ models, sessionId: input.sessionId });
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
resetThreadCheckpoints(input) {
|
|
62
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
63
|
+
yield this.runtime.resetThreadCheckpoints(input.sessionId);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
54
66
|
}
|
|
@@ -17,9 +17,33 @@ export declare class AgentSessionStore {
|
|
|
17
17
|
*/
|
|
18
18
|
appendSteerToCurrentTurn(sessionId: string, steerText: string): Promise<void>;
|
|
19
19
|
getSessionTurns(sessionId: string): Promise<{
|
|
20
|
+
id: any;
|
|
20
21
|
prompt: any;
|
|
21
22
|
response: any;
|
|
22
23
|
}[]>;
|
|
24
|
+
/**
|
|
25
|
+
* Ordered non-system (real conversation) turns with their stored checkpoint id.
|
|
26
|
+
* Used by message editing to locate the target turn and its previous turn's
|
|
27
|
+
* fork point. Stable order (`createdAt ASC, id ASC`) so positions never drift.
|
|
28
|
+
*/
|
|
29
|
+
getAgentTurns(sessionId: string): Promise<{
|
|
30
|
+
id: any;
|
|
31
|
+
prompt: any;
|
|
32
|
+
response: any;
|
|
33
|
+
checkpointId: string | null;
|
|
34
|
+
}[]>;
|
|
35
|
+
/**
|
|
36
|
+
* Apply a message edit and branch: replace the edited turn's prompt and truncate
|
|
37
|
+
* everything after it. Deletes later turns FIRST, then updates the edited turn, so
|
|
38
|
+
* an interruption can only leave a consistent "truncated, edit-not-yet-applied"
|
|
39
|
+
* state (recoverable by retry) — never "edited but stale later turns remain".
|
|
40
|
+
* (AdminForth's resource API has no transactions, hence the deliberate ordering.)
|
|
41
|
+
*/
|
|
42
|
+
editTurnAndTruncateAfter(input: {
|
|
43
|
+
sessionId: string;
|
|
44
|
+
turnId: string;
|
|
45
|
+
newPrompt: string;
|
|
46
|
+
}): Promise<void>;
|
|
23
47
|
getPreviousUserMessages(sessionId: string): Promise<PreviousUserMessage[]>;
|
|
24
48
|
getLatestTurn(sessionId: string): Promise<any>;
|
|
25
49
|
getChatSurfaceSessionId(incoming: ChatSurfaceIncomingMessage): string;
|
|
@@ -29,6 +53,12 @@ export declare class AgentSessionStore {
|
|
|
29
53
|
turnId: string;
|
|
30
54
|
responseText: string;
|
|
31
55
|
debugHistory?: unknown;
|
|
56
|
+
/**
|
|
57
|
+
* Tip checkpoint id for this turn. Persisted only when provided AND the field is
|
|
58
|
+
* configured — so aborted/failed turns (which pass undefined) never overwrite it
|
|
59
|
+
* with a stale value.
|
|
60
|
+
*/
|
|
61
|
+
checkpointId?: string | null;
|
|
32
62
|
}): Promise<void>;
|
|
33
63
|
getResumeState(sessionId: string): Promise<{
|
|
34
64
|
turnId: string;
|