@adminforth/agent 2.2.4 → 2.3.0
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 +76 -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/package.json +1 -1
- 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
|
@@ -35,7 +35,21 @@ export function createAgentSteerQueue({
|
|
|
35
35
|
queue.value = queue.value.filter((item: QueuedMessage) => item.id !== id);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
// Break the assistant's live stream at the steer point: freeze whatever streamed so far
|
|
39
|
+
// (e.g. the current reasoning block), drop the steer message in, and let the rest stream
|
|
40
|
+
// into a fresh assistant message after it — so the conversation reads
|
|
41
|
+
// "reasoning block → steer → new reasoning block" instead of the steer landing next to
|
|
42
|
+
// the user's original prompt.
|
|
43
|
+
//
|
|
44
|
+
// This reaches into the AI SDK's in-flight streaming state (`chat.activeResponse`):
|
|
45
|
+
// - reasoning/text parts are looked up by id reference in `active*Parts`, so pointing
|
|
46
|
+
// those entries at fresh (empty) parts redirects subsequent deltas past the steer;
|
|
47
|
+
// - the stream writer pushes a *new* message whenever `state.message.id` no longer
|
|
48
|
+
// matches the last displayed message, so swapping in a new `message` splits the bubble
|
|
49
|
+
// without duplicating the frozen block.
|
|
50
|
+
// If that internal shape isn't present (not streaming, or a future SDK change), we fall
|
|
51
|
+
// back to inserting the steer just above the streaming assistant.
|
|
52
|
+
async function injectSteerMessage(text: string) {
|
|
39
53
|
const chat = currentChat.value;
|
|
40
54
|
if (!chat) {
|
|
41
55
|
return;
|
|
@@ -46,6 +60,11 @@ export function createAgentSteerQueue({
|
|
|
46
60
|
parts: [{ type: 'text', text, state: 'done' }],
|
|
47
61
|
};
|
|
48
62
|
const messages = chat.messages;
|
|
63
|
+
|
|
64
|
+
if (breakStreamWithSteer(chat, messages, steerMessage)) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
49
68
|
const lastMessage = messages[messages.length - 1];
|
|
50
69
|
if (lastMessage?.role === 'assistant') {
|
|
51
70
|
messages.splice(messages.length - 1, 0, steerMessage);
|
|
@@ -54,6 +73,57 @@ export function createAgentSteerQueue({
|
|
|
54
73
|
}
|
|
55
74
|
}
|
|
56
75
|
|
|
76
|
+
function breakStreamWithSteer(chat: Chat<any>, messages: any[], steerMessage: IMessage): boolean {
|
|
77
|
+
try {
|
|
78
|
+
const streamingState = (chat as any).activeResponse?.state;
|
|
79
|
+
const streamingMessage = streamingState?.message;
|
|
80
|
+
const lastIndex = messages.length - 1;
|
|
81
|
+
const lastMessage = messages[lastIndex];
|
|
82
|
+
const isStreamingIntoLastAssistant =
|
|
83
|
+
streamingMessage?.role === 'assistant'
|
|
84
|
+
&& Array.isArray(streamingMessage.parts)
|
|
85
|
+
&& streamingMessage.parts.length > 0
|
|
86
|
+
&& lastMessage?.role === 'assistant';
|
|
87
|
+
|
|
88
|
+
if (!isStreamingIntoLastAssistant) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 1) Freeze block 1 as a fresh, decoupled assistant message. Copying the parts (and
|
|
93
|
+
// reassigning the array slot) detaches it from the SDK's live parts and forces a
|
|
94
|
+
// re-render so the block stops showing as "streaming".
|
|
95
|
+
const frozenParts = streamingMessage.parts.map((part: any) => ({
|
|
96
|
+
...part,
|
|
97
|
+
state: part?.state === 'streaming' ? 'done' : part?.state,
|
|
98
|
+
}));
|
|
99
|
+
messages[lastIndex] = { ...streamingMessage, parts: frozenParts };
|
|
100
|
+
// 2) Redirect the SDK's in-flight reasoning/text parts into a fresh continuation
|
|
101
|
+
// message so subsequent deltas (resolved by id reference) land after the steer.
|
|
102
|
+
const continuation: any = { id: crypto.randomUUID(), role: 'assistant', parts: [] };
|
|
103
|
+
const redirectActiveParts = (activeParts: Record<string, any> | undefined) => {
|
|
104
|
+
if (!activeParts) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
for (const id of Object.keys(activeParts)) {
|
|
108
|
+
const continued = { ...activeParts[id], text: '', state: 'streaming' };
|
|
109
|
+
activeParts[id] = continued;
|
|
110
|
+
continuation.parts.push(continued);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
redirectActiveParts(streamingState.activeReasoningParts);
|
|
114
|
+
redirectActiveParts(streamingState.activeTextParts);
|
|
115
|
+
|
|
116
|
+
// 3) Hand the SDK the continuation as its streaming message; its next write() pushes
|
|
117
|
+
// it as a new assistant bubble. Slot the steer between the frozen block and it.
|
|
118
|
+
streamingState.message = continuation;
|
|
119
|
+
messages.push(steerMessage);
|
|
120
|
+
return true;
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error('Steer stream-break failed, inserting steer inline instead', error);
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
57
127
|
// Persist the steer by appending it onto the running turn's prompt (display only — the
|
|
58
128
|
// LLM already has it via the running turn + checkpointer, so this never re-enters the
|
|
59
129
|
// model context). The backend adds the marker; the reload path splits it back out.
|
|
@@ -104,11 +174,16 @@ export function createAgentSteerQueue({
|
|
|
104
174
|
void sendMessage(next.text);
|
|
105
175
|
}
|
|
106
176
|
|
|
177
|
+
function clear() {
|
|
178
|
+
queue.value = [];
|
|
179
|
+
}
|
|
180
|
+
|
|
107
181
|
return {
|
|
108
182
|
queue,
|
|
109
183
|
enqueue,
|
|
110
184
|
removeQueuedMessage,
|
|
111
185
|
steerQueuedMessage,
|
|
112
186
|
flushNext,
|
|
187
|
+
clear,
|
|
113
188
|
};
|
|
114
189
|
}
|
|
@@ -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
|
|
package/dist/custom/package.json
CHANGED
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
|
}
|